<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[葉は静かに落ちる]]></title><description><![CDATA[有朋自远方来, 不亦说乎]]></description><link>https://sin6626.me</link><image><url>https://sin6626.mehttps://sin6626.me/Devil.ico</url><title>葉は静かに落ちる</title><link>https://sin6626.me</link></image><generator>Shiro (https://github.com/Innei/Shiro)</generator><lastBuildDate>Sun, 20 Sep 2026 18:50:58 GMT</lastBuildDate><atom:link href="https://sin6626.me/feed" rel="self" type="application/rss+xml"/><pubDate>Sun, 20 Sep 2026 18:50:58 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[出租屋的最后一晚]]></title><description><![CDATA[<link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/z42vgw8wv2ohumiety.png"/><link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/5x2bbx4lx6wjj1k8tp.png"/><link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/u3fcmagzb5q255l5ui.png"/><link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/87yrqbmzu7q4nj89tq.png"/><link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/k0j121rfgufg447znc.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://sin6626.me/notes/10">https://sin6626.me/notes/10</a></blockquote><div><p>|<em>&amp;#32;两个月的暑假转瞬而过, 今天是我在出租屋的最后一晚, 记录总结下这两个月做的事情</em></p><h2 id="flag">刚进出租屋立Flag都实现了吗？</h2><p>Flag关联文章是是这个<a href="https://sin6626.me/notes/6">社会篇: 第一天的实习 - 葉は静かに落ちる</a>。刚刚住进出租屋实习的时候给自己立了三个搬出出租屋时候要完成的Flag，原本是打算实习三个月的，但是最后却只有两个月，导致完成Flag时间很紧张，不过好在最后没有辜负自己的期望，完成了。</p><p><img src="https://s3.sin6626.me/mx-images/z42vgw8wv2ohumiety.png" alt="image.png"/></p><p>关联文章</p><ol start="1"><li><a href="https://sin6626.me/posts/study&amp;think/nestjs-learning">NestJS学习 - 葉は静かに落ちる</a></li><li><a href="https://sin6626.me/posts/study&amp;think/react-vs-vue-learning-experience">React学习心得分享: React 对比 Vue - 葉は静かに落ちる</a></li></ol><p>LeetCode截图：</p><p><img src="https://s3.sin6626.me/mx-images/5x2bbx4lx6wjj1k8tp.png" alt="260489ad7aaef46be561a4d39b6680b4.png"/></p><p><img src="https://s3.sin6626.me/mx-images/u3fcmagzb5q255l5ui.png" alt="image.png"/></p><p>Nest和React都还没有正式的去做项目，项目大部分还是Vue和Nuxt的，毕竟时间已经不够了，不过我后面会去做的，Nest和React（Next也学完了，不过没发文）我一直都认为是一个非常好的技术，有非常优秀的开源项目，学习的资源很多。慢慢来吧，一个人慢慢走，累了就歇歇，然后继续走。</p><h2 id="">更多的技术</h2><p>2个月时间我并不是只把三个Flag主线任务给完成了，还做完了很多的支线任务。有很多很多，简单例举下。</p><p> 更加绑定了Nuxt生态，如NuxtUI，NuxtContent，NuxtStudio，NuxtAuth，Nuxt18n；动画方面也研读了很多大佬的项目，学到了很多与动画有关的技术或者是CSS属性，对于动画的实现有更加清晰的认知，有用css的clip-path做画面的切割，用stroke-dasharray + stroke-dashoffset做SVG简笔动画，用Motion做更加通用的动画，善用spring预设，Motion的Layout和LayoutId也是一把好手，用GASP可以做命令式更加复杂的动画，配合MorphSVG，ScrollTrigger，SplitText等插件实现更多动画，number-flow可以做钟表般富有机械感，精细的时间转动动画；还有Web 3D，OpenGL，WebGL，Three实现3D效果，使用Glsl做Shader编程，用GPU加速渲染等。大部分的技术都是看别人项目源码，看官方文档，问Ai学习的，毕竟大部分的时间要上班，上班也不能看视频学习。</p><p>为此我开了一个项目去收藏记录我学到的UI和动画技术，项目是纯静态的，还没加入后端，最近其实我还去实现一个IM系统，那时候会加上的吧。<a href="https://ui-animation-collection.vercel.app/animation/interactive-icon-morph">ui-animation-collection.vercel.app/animation/interactive-icon-morph</a>。项目感觉我个人认为最惊艳的地方是音频3D可视化，忘记说了，项目没有部署在自己的服务器上，是在Vercel上，得开魔法网络才能进去看。</p><p><img src="https://s3.sin6626.me/mx-images/87yrqbmzu7q4nj89tq.png" alt="image.png"/></p><h2 id="">其他所获</h2><p>给GSAP官方文档提出了一个建议，被官方采纳了，有关文章是这个<a href="https://sin6626.me/notes/7">gsap官方文档的更新和Svg线条动画的探索 - 葉は静かに落ちる</a></p><p><img src="https://s3.sin6626.me/mx-images/k0j121rfgufg447znc.png" alt="image.png"/></p><p>那时候感觉很高兴，很激动，逢人就说，哈哈哈哈哈。</p><h3 id="">公司所获</h3><p>这次实习是一个很小的公司，才注册4年不到，注册资50w，除去老板一共5个正式人员，但是在公司也收获不少。第一次接触了Android开发相关的知识（虽然不是真正做Android开发），用到了很多原生Android的模块，Accessbility无障碍，NotificationServer做控制栏监听，还有Content去可以读取短信数据库什么的。也懂了很多项目调试的方式，CICD部署项目，前后端兄弟们应该如何联调，一个好的后端应该具备什么样的品质（被后端文档折磨过的感悟）。还有就是坚定了我放弃Uniapp这个技术栈的决心，Uniapp如果是写小程序可以用下，但是App（也是我转向ReactNative的原因）我是真的不想用了，DX拉完了，第一次觉得有Ai了开发还是如此的痛苦。</p><p>还认识了公司里的同事们，感觉跟同行的人说话更加轻松愉快。</p><p>不过听说我前脚刚走，公司好像出了问题，把三个正式工给开了，好像是项目没做好，要打官司，不过具体的并不清楚，信息也是听走的晚的人说的，也希望公司能挺过难关吧，感觉老板人和技术人还是很不错的。</p><h2 id="">下一步计划</h2><p>打物联网的比赛，赛后继续消化Innei24年的项目，尝试今年跟大四的抢下秋招，春招的实习岗位，学习ReactNative，继续学习Nest，学习更多的设计，看了月之暗面的招聘信息，打算补上在移动端的技术短板，目标是Android原生（虽然听别人已死...），持续算法学习，我马上大三上，还有一年的时间，还是有可能在明年的秋招以2K的LeetCode刷题量进场的，群友也是推荐我继续刷算法，那再刷一年。还有就是要往Agent方面靠近了，已经盯上了一个技术了，洛鱼的用Vue组件形式未作Agent信息载体的技术。</p><p><ins><em>继续努力吧，累了就歇歇，然后继续走下去，当第一颗卫星飞向大气层外，我们便以为自己终有一日会征服宇宙</em></ins>。我靠，忘记说日语方面了，不管了， 算了算了</p></div><p style="text-align:right"><a href="https://sin6626.me/notes/10#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://sin6626.me/notes/10</link><guid isPermaLink="true">https://sin6626.me/notes/10</guid><dc:creator><![CDATA[Sin]]></dc:creator><pubDate>Fri, 04 Sep 2026 16:05:38 GMT</pubDate></item><item><title><![CDATA[基于 View Transitions 的主题切换水波动画圆心偏移排查与重构]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://sin6626.me/posts/technology/view-transitions-api-theme-switch-animation-origin-analysis">https://sin6626.me/posts/technology/view-transitions-api-theme-switch-animation-origin-analysis</a></blockquote><div><h2 id="-ui-">一、问题起因：一次 UI 开发中的严重坐标偏移</h2><p>今天下午，开发者 <strong>sin</strong> 在调试其前端 UI 动画合集项目时，测试了之前编写的一个深浅色主题切换按钮。该按钮基于现代浏览器原生的 <strong>View Transitions API</strong> 打造，预期效果是以鼠标点击位置为圆心，通过 <code>clip-path</code> 裁切出一个正圆水波，丝滑地向全屏扩散展开。</p><p>然而在交互实测中，sin 发现了一个无法忽视的视觉瑕疵：<strong>水波扩散的圆心并没有对准鼠标的点击位置，而是发生了肉眼可见的大幅空间偏位</strong>。</p><p>sin 最初编写这个按钮时，参考了开源社区的流行做法（包括 Nuxt 官方示例库中广泛流传的实现）。其核心逻辑是在事件触发后，直接通过 Web Animations API（WAAPI）对根快照伪元素 <code>::view-transition-new(root)</code> 注入关键帧动画：</p><pre class="language-ts lang-ts"><code class="language-ts lang-ts">// 社区常见的早期写法：利用 TS/JS 计算坐标后直接通过 WAAPI 操纵伪元素
const x = event.clientX
const y = event.clientY
const endRadius = Math.hypot(
  Math.max(x, window.innerWidth - x),
  Math.max(y, window.innerHeight - y)
)

const transition = document.startViewTransition(() =&gt; {
  switchTheme()
})

transition.ready.then(() =&gt; {
  document.documentElement.animate(
    {
      clipPath: [
        `circle(0px at ${x}px ${y}px)`,
        `circle(${endRadius}px at ${x}px ${y}px)`
      ]
    },
    {
      duration: 600,
      easing: &#x27;cubic-bezier(.76,.32,.29,.99)&#x27;,
      pseudoElement: &#x27;::view-transition-new(root)&#x27;
    }
  )
})</code></pre><p>这段逻辑在代码组织上极为诱人：坐标获取、半径推导、动画触发全在 TypeScript 函数闭包内闭环，既无需额外维护动态的 CSS class，也省去了清理工作。</p><p>但正是这段看似优雅的代码，在高分屏设备上产生了严重的圆心错位。</p><hr/><h2 id="-sin-">二、假设推演与 sin 的关键证伪</h2><p>当 sin 将该问题抛给我（Gemini 3.8 Flash）协助排查时，作为一个大语言模型，我的第一反应也落入了常规前端 Bug 的经验主义推测中。我先后向 sin 提出了几种可能性：</p><ol start="1"><li>是否页面存在滚动（<code>scrollX / scrollY</code>），导致 <code>clientX/Y</code> 需要补偿？</li><li>按钮的外层父级容器是否存在 CSS <code>transform</code> 或 <code>zoom</code>，导致视口局部坐标系发生了缩放或位移？</li><li>是否是 Vue 响应式状态更新与 DOM 渲染的时序差异，导致过渡快照截取的时机不对？</li></ol><p>面对我的这些推论，sin 并没有盲从，而是凭借敏锐的技术嗅觉，迅速提出了两个极具杀伤力的<strong>关键反例</strong>，彻底推翻了我最初的方向：</p><h3 id="1-">1. 官方标杆项目的同台“翻车”</h3><p>sin 亲自去访问了 NuxtUI 官方开源模板 —— <a href="https://portfolio-template.nuxt.dev/">Emma Thompson - Portfolio Template</a> 的线上演示站。
测试结果令人吃惊：<strong>连这个官方模板上的主题切换按钮，在新版浏览器和不同电脑上也同样发生了巨大的圆心偏移！而且在不同的电脑和缩放比下，偏移的距离还不尽相同。</strong>
这直接证明了该 Bug 绝非当前项目私有的布局或时序缺陷，而是整个前端社区流传的这套方案本身在现代浏览器底层出现了系统性失效。</p><h3 id="2--css-">2. 纯 CSS 对照组的精准表现</h3><p>紧接着，sin 指出了项目中另一个决定性的对照组：在当前的组件库中，sin 之前还编写过另一个使用 <strong>纯 CSS + CSS 变量</strong> 控制 <code>clip-path</code> 的对照按钮。
在完全相同的页面结构、完全相同的事件监听以及完全相同的 <code>(event.clientX, event.clientY)</code> 坐标输入下，<strong>该 CSS 按钮在所有设备上均做到了 0 像素偏差的精准对齐，指哪打哪！</strong></p><p>sin 的这两记关键反驳，立刻将我们的排查视野从“业务层代码找茬”，直接拉升到了<strong>“Chromium 渲染引擎对 WAAPI 伪元素动画的底层解析机理”</strong>之上。</p><hr/><h2 id="chromium--dpr-">三、深层机理：Chromium 渲染管线与 DPR 坐标脱节</h2><p>顺着 sin 锁定的突破口，我和 sin 共同深入剖析了 Chromium 内核对于 View Transitions 规范的演进历程，终于还原了事情的完整真相。</p><h3 id="1-snapshot-root">1. 从扁平快照到独立顶层快照（Snapshot Root）</h3><p>在 View Transitions API 刚诞生的早期版本（Chrome 111 ~ 125）：</p><ul><li>伪元素 <code>::view-transition-new(root)</code> 在 Chromium 内部的处理较为原始，很大程度上被当作根文档视口（Viewport）的一个普通绘制层；</li><li>当开发者调用 <code>document.documentElement.animate</code> 传入 <code>pseudoElement</code> 时，合成器线程（Compositor）直接将传入的静态像素坐标映射在当前页面的可视视口（Visual Viewport）上，因而早期未暴露出明显的肉眼偏移。</li></ul><p>但随着 <strong>W3C View Transitions Level 2 规范</strong>的正式推进（从 Chrome 126+ 引入跨文档过渡，到 Chrome 130+ 引入 Scoped 容器过渡，直至如今最新的 Chrome 150+ 内核）：</p><ul><li>规范对快照伪元素树进行了彻底重构，<code>::view-transition</code> 被定义为一个<strong>脱离普通文档流、独立挂载在顶级图层（Top Layer）上的快照根容器（Snapshot Root）</strong>；</li><li>为了支持视口缩放与抗锯齿，Blink 渲染引擎为快照伪元素树（<code>::view-transition-group</code>）引入了独立的<strong>变换矩阵（Transform Matrix）与栅格化基准表面</strong>。</li></ul><h3 id="2-waapi--dpr-">2. WAAPI 伪元素动画与非整数 DPR 的二次畸变</h3><p>当开发者通过 TypeScript 调用：</p><pre class="language-ts lang-ts"><code class="language-ts lang-ts">document.documentElement.animate(
  { clipPath: `circle(...) at ${x}px ${y}px` },
  { pseudoElement: &#x27;::view-transition-new(root)&#x27; }
)</code></pre><p>这段字符串被直接传递给合成器。然而在现代 Chromium 中，作用在包含独立变换矩阵的快照伪元素层上的 WAAPI 动画，<strong>未能正确依据当前的 CSS 布局视口进行逆矩阵换算</strong>。</p><p>尤其在 Windows 系统常见的高分屏环境（<strong>设备像素比 DPR ≠ 1.0，如笔记本常见的 125% 或 150% 缩放</strong>）下，快照图层矩阵的物理坐标换算与逻辑像素之间产生了成倍的比例畸变。这完全解释了 sin 所观察到的“每台电脑偏差都不一样”的现象 —— 只要屏幕缩放比或分辨率不同，变换矩阵的偏移倍率就完全不同。</p><h3 id="3--sin--css-">3. 为什么 sin 编写的 CSS 方案完全免疫？</h3><p>CSS 变量方案的数据流向是：
$$\text{点击坐标 (x, y)} \xrightarrow{\text{写入}} \text{:root CSS 自定义变量} \xrightarrow{\text{样式计算}} \text{CSS @keyframes 逐帧求值}$$</p><p>由于 CSS 变量直接挂载在 <code>&lt;html&gt;</code>（<code>:root</code>）根节点上，由浏览器的<strong>样式计算引擎（Style Engine）</strong>在常规文档流的上下文中统一求值。在此处计算出的几何尺寸天生遵循标准 CSS 视口规则并自动适配 DPR 缩放，彻底绕过了合成器层对独立快照伪元素的坐标换算缺陷。</p><hr/><h2 id="0-">四、工程重构：0 偏差纯单向水波方案落地</h2><p>在彻底厘清根因之后，sin 对组件提出了更进一步的体验要求：</p><ol start="1"><li><strong>统一为纯单向水波扩散（Pure Expand）</strong>：传统模板在深色切浅色时常采用反向收缩动效（Contract），视觉上极具压迫感。sin 要求无论深切浅还是浅切深，统一以点击位置为圆心向外全屏绽放覆盖。</li><li><strong>严禁改动原有参数</strong>：完整保留原有的 <code>600ms</code> 动画时长、<code>cubic-bezier(.76, .32, .29, .99)</code> 缓动曲线以及 <code>Math.hypot</code> 外接圆半径覆盖算法。</li><li><strong>消除白屏闪烁</strong>：在视图过渡回调执行时同步更新 DOM 类名，确保快照捕获即时准确。</li></ol><p>我们最终落地的重构代码如下：</p><h3 id="1-vue-3--typescript">1. 脚本逻辑（Vue 3 / TypeScript）</h3><pre class="language-vue lang-vue"><code class="language-vue lang-vue">&lt;script setup lang=&quot;ts&quot;&gt;
const colorMode = useColorMode()

// 计算下一个目标主题
const nextTheme = computed(() =&gt; (colorMode.value === &#x27;dark&#x27; ? &#x27;light&#x27; : &#x27;dark&#x27;))

// 同步修改 DOM class，确保 startViewTransition 内部拍摄到目标主题快照
const switchTheme = () =&gt; {
  const next = nextTheme.value
  const html = document.documentElement
  html.classList.remove(`${colorMode.value}-mode`)
  html.classList.add(`${next}-mode`)
  colorMode.preference = next
}

const startViewTransition = (event: MouseEvent) =&gt; {
  // 降级判断：不支持 View Transitions API 或系统开启了减弱动态效果
  if (!document.startViewTransition || window.matchMedia(&#x27;(prefers-reduced-motion: reduce)&#x27;).matches) {
    switchTheme()
    return
  }

  const x = event.clientX
  const y = event.clientY
  const endRadius = Math.hypot(
    Math.max(x, window.innerWidth - x),
    Math.max(y, window.innerHeight - y)
  )

  const root = document.documentElement
  const transitionClass = &#x27;sinui-theme-reveal-expand&#x27;

  // 将坐标与半径写入根元素 CSS 变量
  root.style.setProperty(&#x27;--sinui-theme-transition-x&#x27;, `${x}px`)
  root.style.setProperty(&#x27;--sinui-theme-transition-y&#x27;, `${y}px`)
  root.style.setProperty(&#x27;--sinui-theme-transition-radius&#x27;, `${endRadius}px`)
  root.classList.add(transitionClass)

  const transition = document.startViewTransition(() =&gt; {
    switchTheme()
  })

  // 过渡结束后清理 CSS 变量与辅助 class
  transition.finished.finally(() =&gt; {
    root.classList.remove(transitionClass)
    root.style.removeProperty(&#x27;--sinui-theme-transition-x&#x27;)
    root.style.removeProperty(&#x27;--sinui-theme-transition-y&#x27;)
    root.style.removeProperty(&#x27;--sinui-theme-transition-radius&#x27;)
  })
}
&lt;/script&gt;

&lt;template&gt;
  &lt;ClientOnly&gt;
    &lt;UButton
      :aria-label=&quot;`Switch to ${nextTheme} mode`&quot;
      :icon=&quot;`i-lucide-${nextTheme === &#x27;dark&#x27; ? &#x27;sun&#x27; : &#x27;moon&#x27;}`&quot;
      color=&quot;neutral&quot;
      variant=&quot;ghost&quot;
      size=&quot;sm&quot;
      class=&quot;rounded-full&quot;
      @click=&quot;startViewTransition&quot;
    /&gt;
    &lt;template #fallback&gt;
      &lt;div class=&quot;size-4&quot; /&gt;
    &lt;/template&gt;
  &lt;/ClientOnly&gt;
&lt;/template&gt;</code></pre><h3 id="2-css">2. 样式实现（CSS）</h3><pre class="language-css lang-css"><code class="language-css lang-css">/* 禁用默认 cross-fade 动画，防止与 clip-path 产生双重图层叠加混合 */
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

/* 旧视图垫在底层，新视图在顶层自点击原点向外扩散覆盖 */
.sinui-theme-reveal-expand::view-transition-old(root) {
  z-index: 1;
}

.sinui-theme-reveal-expand::view-transition-new(root) {
  z-index: 9999;
  clip-path: circle(
    0 at
    var(--sinui-theme-transition-x, 50vw)
    var(--sinui-theme-transition-y, 50vh)
  );
  animation: sinui-theme-reveal-expand 600ms cubic-bezier(.76, .32, .29, .99) both;
}

@keyframes sinui-theme-reveal-expand {
  to {
    clip-path: circle(
      var(--sinui-theme-transition-radius, 150vmax) at
      var(--sinui-theme-transition-x, 50vw)
      var(--sinui-theme-transition-y, 50vh)
    );
  }
}</code></pre><hr/><h2 id="view-transitions-level-2--types-">五、未来展望：View Transitions Level 2 的 <code>types</code> 特性</h2><p>在讨论过程中，我和 sin 还探讨了 View Transitions Level 2 的演进趋势。对于 Chrome 125+ 及 Safari 18+ 等现代环境，规范原生提供了 <code>types</code> 特性。</p><p>利用该特性，开发者未来甚至可以彻底省去在根节点增删 class 的操作：</p><pre class="language-ts lang-ts"><code class="language-ts lang-ts">// Level 2 现代类型声明调用
document.startViewTransition({
  update: switchTheme,
  types: [&#x27;theme-expand&#x27;]
})</code></pre><p>CSS 直接配合原生伪类匹配：</p><pre class="language-css lang-css"><code class="language-css lang-css">html:active-view-transition-type(theme-expand)::view-transition-new(root) {
  z-index: 9999;
  clip-path: circle(0 at var(--sinui-theme-transition-x) var(--sinui-theme-transition-y));
  animation: sinui-theme-reveal-expand 600ms cubic-bezier(.76, .32, .29, .99) both;
}</code></pre><p>过渡类型会在动画周期内由浏览器原生激活与注销，实现更加纯粹解耦的状态控制。</p><hr/><h2 id="">六、结对复盘：人机协作中的思考</h2><p>这次我与 sin 共同排查并解决问题的全过程，为我们留下了两点深刻的工程启示：</p><ol start="1"><li><strong>破除对权威开源模板的盲信，善用对照实验</strong>：<br/>像 Emma Thompson 这样NuxtUI的官方的模板，其代码编写于 API 刚刚诞生之初。当底层规范和浏览器引擎发生重构后，曾经的“最佳实践”往往会演变成“技术暗礁”。在排查时，sin 没有盲目纠结于“是不是我代码写错了”，而是通过拉取官方在线环境交叉比对，并利用自身编写的 CSS 对照组快速切断无效假设，这是本次能够迅速击中内核痛点的关键所在。</li><li><strong>合理的工程职责边界划分</strong>：<br/>JavaScript / TypeScript 应当聚焦于业务状态管理与事件输入，而图层裁切、补间动画以及复杂的几何计算，交给 CSS 样式引擎处理往往更加健壮。<strong>“JS 提取变量暴露给 Custom Properties，CSS 原生消费变量驱动 Keyframes”</strong>，不仅亲和 GPU 硬件加速，更能天然获得现代浏览器对多倍率屏幕（DPR）的精准适配能力。</li></ol></div><p style="text-align:right"><a href="https://sin6626.me/posts/technology/view-transitions-api-theme-switch-animation-origin-analysis#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://sin6626.me/posts/technology/view-transitions-api-theme-switch-animation-origin-analysis</link><guid isPermaLink="true">https://sin6626.me/posts/technology/view-transitions-api-theme-switch-animation-origin-analysis</guid><dc:creator><![CDATA[Sin]]></dc:creator><pubDate>Fri, 04 Sep 2026 14:24:28 GMT</pubDate></item><item><title><![CDATA[React学习心得分享: React 对比 Vue]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://sin6626.me/posts/study&amp;think/react-vs-vue-learning-experience">https://sin6626.me/posts/study&amp;think/react-vs-vue-learning-experience</a></blockquote><div><h1 id="-"> </h1><blockquote><p><strong>前言</strong>：一直以来，Vue 都是我的主力前端框架。从模板语法、响应式数据到组合式 API，Vue 的“开箱即用”和“保姆级体验”伴随我完成了许多项目。最近，我系统地学习了小满老师的 <a href="https://message163.github.io/react-docs/react/basic/introduce.html">React Docs 文档</a>，将 React 作为我的第二门前端框架正式纳入口袋。</p><p>虽然目前我刚刚学完整个教程、还没有在实际商业项目中完整踩过实战的坑，但这套体系化学习下来，我最大的感受就是：<strong>React 和 Vue 的底层世界观完全不同。</strong></p><p>下面对照这份 React 文档的知识脉络（<strong>基础语法、编译工具、底层原理、组件体系、CSS 方案、Hooks 全家桶、路由与状态管理</strong>），结合我熟悉的 Vue，写下这份初学者视角的框架对比与心得笔记。</p></blockquote>
<hr/><h2 id="1--vs--js">1. 入门与语法：模板指令 vs “一切皆是 JS”</h2><p>打开文档的第一章，就是 TSX（JSX）语法。对于习惯了 Vue 模板语法的开发者来说，这是第一道冲击。</p><table><thead><tr><th>业务场景</th><th>Vue (Template 语法糖)</th><th>React (TSX / JS 表达式)</th><th>体验感受</th></tr></thead><tbody><tr><td>数据插值</td><td><code>{{ message }}</code></td><td><code>{message}</code></td><td>React 用单大括号，里面可以写<em></em>任意合法的 JS 表达式。</td></tr><tr><td>条件渲染</td><td><code>v-if=&quot;flag&quot;</code> / <code>v-else</code></td><td><code>flag ? &lt;A /&gt; : &lt;B /&gt;</code> 或 <code>flag &amp;&amp; &lt;A /&gt;</code></td><td>Vue 是指令化思维；React 完全靠原生 JS 的三元运算符和短路求值。</td></tr><tr><td>列表渲染</td><td><code>v-for=&quot;item in list&quot; :key=&quot;item.id&quot;</code></td><td><code>list.map(item =&gt; &lt;li key={item.id}&gt;{item.name}&lt;/li&gt;)</code></td><td>React 没有特定指令，就是原生数组的 <code>.map()</code>。</td></tr><tr><td>事件绑定</td><td><code>@click=&quot;handleClick&quot;</code></td><td><code>onClick={handleClick}</code></td><td>驼峰命名，传函数引用。</td></tr><tr><td>类名绑定</td><td><code>:class=&quot;{ active: isActive }&quot;</code></td><td><code>className={isActive ? &#x27;active&#x27; : &#x27;&#x27;}</code></td><td>React 中 <code>class</code> 是 JS 关键字，必须写 <code>className</code>；<code>for</code> 需写成 <code>htmlFor</code>。</td></tr></tbody></table><h3 id="">学习感受：</h3><ul><li><strong>Vue 的感受是“规整”</strong>：Vue 给开发者设计好了各种指令（<code>v-if</code>, <code>v-for</code>, <code>v-model</code>），模板结构清晰，HTML 该长什么样就是什么样。</li><li><strong>React 的感受是“自由”</strong>：TSX 根本不是模板，它本质上就是 <strong>JavaScript 语法扩展</strong>。不用记框架自创的指令语法，只要 JS 基础好，就可以随意用 <code>if/else</code>、<code>map</code>、<code>filter</code> 来组装 UI。</li></ul><hr/><h2 id="2-babel--swc-vs-vue-sfc-compiler">2. 构建与编译工具：Babel / SWC vs Vue SFC Compiler</h2><p>文档在“工具篇”特意讲解了 <strong>Babel</strong> 与 <strong>SWC</strong>。</p><ul><li><strong>React 的转译本质</strong>：我们写的 <code>&lt;div className=&quot;box&quot;&gt;Hello&lt;/div&gt;</code>，经由 Babel 或 SWC 转译后，本质上是变成 <code>React.createElement(&#x27;div&#x27;, { className: &#x27;box&#x27; }, &#x27;Hello&#x27;)</code>（或者新的 <code>_jsx(&#x27;div&#x27;, ...)</code>）。</li><li><strong>对比 Vue</strong>：Vue 的核心是 SFC（单文件组件 <code>.vue</code>），由 <code>@vitejs/plugin-vue</code> 解析，将 <code>&lt;template&gt;</code> 编译为高效的 render 函数，并将 <code>&lt;script&gt;</code> 和 <code>&lt;style&gt;</code> 分离处理。</li></ul><h3 id="">学习感受：</h3><p>通过文档手写 Babel/SWC 转译示例后我才明白：<strong>React 的组件真的就是一个返回虚拟 DOM 对象的纯 JS 函数</strong>，它没有 Vue SFC 那么强的结构专属感。</p><hr/><h2 id="3-">3. 核心原理篇：不可逆的时间切片与细粒度响应式</h2><p>文档中关于 <strong>VDOM、Fiber、Diff 以及&amp;#32;<code>requestIdleCallback</code></strong> 的章节是我读得最过瘾、也最烧脑的部分。</p><h3 id="-fiber-">① Fiber 架构与双缓存机制</h3><ul><li><strong>React 的痛点与解法</strong>：早期的 React 递归遍历 VDOM 树，如果组件树太深就会长期霸占主线程导致页面掉帧卡顿。于是 React 16 彻底重构为 <strong>Fiber</strong> 架构，把树状结构变成了<strong>链表结构</strong>，借助浏览器空闲调度（类似 <code>requestIdleCallback</code>）实现<strong>可中断、可恢复的异步更新（时间切片）</strong>。并通过“当前树 (current)”与“工作树 (workInProgress)”双缓存交替来提高渲染稳定性。</li><li><strong>Vue 的解法（靶向更新）</strong>：Vue 根本不需要 Fiber 这种复杂的时间切片！因为 Vue 拥有基于 Proxy 的<strong>精准依赖收集</strong>。Vue 在编译时做了大量静态标记（PatchFlag、静态提升），数据变化时，直接精准定位到对应的组件或具体 DOM 节点进行打补丁，更新开销极小。</li></ul><h3 id="-diff-">② Diff 算法差异</h3><ul><li><strong>React Diff</strong>：同层比较，仅支持向右移动单向比较，若 key 匹配但位置靠前，可能会引发多余的移动操作。</li><li><strong>Vue Diff</strong>：Vue 2 采用<strong>双端 Diff</strong>（头头、尾尾、头尾交叉对比），Vue 3 采用基于<strong>最长递增子序列的快速 Diff</strong>，在 DOM 移动次数的优化上更加极致。</li></ul><hr/><h2 id="4-">4. 组件体系：单向流动的严谨与灵活</h2><p>文档中的组件篇涵盖了组件通信、受控组件、Portal、Suspense 和 HOC（高阶组件）。</p><h3 id="--vs-v-model">① 受控组件 vs <code>v-model</code></h3><p>这是初学 React 时最容易手忙脚乱的地方：</p><ul><li><strong>Vue</strong>：一个 <code>v-model=&quot;text&quot;</code> 搞定双向绑定。</li><li><strong>React</strong>：必须显式声明为<strong>受控组件</strong>：</li></ul><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">  &lt;input value={text} onChange={(e) =&gt; setText(e.target.value)} /&gt;</code></pre>数据流完全单向，状态必须从上往下传，改动必须通过事件触发通知更新。
<h3 id="--vs-children--render-props">② 插槽 vs <code>children</code> &amp; Render Props</h3><ul><li><strong>Vue</strong>：拥有极其完善的插槽系统（默认插槽、具名插槽 <code>&lt;template #header&gt;</code>、作用域插槽）。</li><li><strong>React</strong>：默认内容直接通过 <code>props.children</code> 获取；如果需要作用域插槽的效果，直接传一个函数即可（<strong>Render Props</strong> 模式：<code>props.render(data)</code>），纯 JS 函数传参，简单直接。</li></ul><h3 id="-">③ 传送组件与异步组件</h3><ul><li>传送组件：React 的 <code>createPortal(JSX, domNode)</code> 对标 Vue 的 <code>&lt;Teleport to=&quot;body&quot;&gt;</code>。</li><li>异步加载：React 的 <code>lazy(() =&gt; import(...)) + &lt;Suspense fallback={&lt;Loading /&gt;} /&gt;</code> 对标 Vue 的 <code>defineAsyncComponent + &lt;Suspense&gt; (但是这部分我在Vue里面没怎么用过...)</code>。</li></ul><h3 id="--hoc">④ 高阶组件 (HOC)</h3><p>React 拥有丰富的高阶组件文化（接收组件并返回增强后的新组件），而 Vue 社区在进入 Vue 3 后基本统一推崇 Composition API，极少使用 HOC, React在更多的Hooks支持下, HOC的使用场景也被大大压缩, 平时开发听小满说根本用不到。</p><hr/><h2 id="5-css--vs-">5. CSS 方案：自由探索 vs 开箱即用</h2><p>在 Vue 里，我们最习惯的写法是：</p><pre class="language-vue lang-vue"><code class="language-vue lang-vue">&lt;style scoped&gt;
.title { color: red; }
&lt;/style&gt;</code></pre><p><code>scoped</code> 属性通过 PostCSS 自动给 DOM 添加 <code>data-v-xxx</code> 属性选择器，干净又好用。</p><p>而在 React 文档中，CSS 方案是一场“百家争鸣”：</p><ol start="1"><li><p><strong>CSS Modules</strong>：文件命名为 <code>index.module.css</code>，像对象一样 <code>import styles from &#x27;./index.module.css&#x27;</code>，通过 <code>className={styles.title}</code> 调用，生成哈希类名隔离, 说到底是没有Vue那种scoped隔离的痛。</p></li><li><strong>CSS-in-JS</strong>（如 styled-components / emotion）：把样式直接作为组件来写, Ant Design就是用的这种方式写的样式。</li><li><strong>Tailwind CSS</strong>：原子化 CSS，文档里重点推荐，由于写在 <code>className</code> 里很自然，在 React 社区极为流行, 当然在Vue的圈子里面也极为的流行, 说到底就是极为的流行。</li></ol><hr/><h2 id="6-react-hooks--vs-vue--api">6. 核心重头戏：React Hooks 全家桶 vs Vue 组合式 API</h2><p>读完整份文档，Hooks 是篇幅最大、也是和 Vue 3 Composition API 思想碰撞最激烈的部分。</p><h3 id="-usestate-vs-ref--reactive">① <code>useState</code> vs <code>ref / reactive</code></h3><ul><li><strong>Vue</strong>：数据是“可变响应式代理”。</li></ul><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">  const count = ref(0)
  count.value++ // 直接修改，Proxy 自动捕获并派发更新</code></pre><ul><li><strong>React</strong>：强调<strong>不可变数据（Immutable）（很傻感觉，DX很不好，得用第三方useImmer去加强DX）</strong>。</li></ul><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">  const [count, setCount] = useState(0)
  setCount(count + 1) // 必须显式调用 setter，不能直接 count = 1</code></pre><h3 id="-useeffect-vs-watch--watcheffect--">② <code>useEffect</code> vs <code>watch / watchEffect / 生命周期</code></h3><p>在 Vue 中，挂载、卸载、监听是各司其职的（<code>onMounted</code>、<code>onUnmounted</code>、<code>watch</code>）。<br/>而在 React 中，文档把所有的“副作用”都归拢到了 <strong><code>useEffect</code></strong> 中：</p><ul><li>依赖项传 <code>[]</code>：相当于 <code>onMounted</code>，返回的清理函数相当于 <code>onUnmounted</code>。</li><li>依赖项传 <code>[count]</code>：相当于 <code>watch(count, ...)</code>。</li><li>文档还介绍了更细分的 <code>useLayoutEffect</code>（DOM 变更后、浏览器绘制前同步执行，防闪烁）以及 <code>useInsertionEffect</code>（专为 CSS-in-JS 插入样式设计）。</li></ul><h3 id="-usememo--usecallback-vs-computed">③ <code>useMemo</code> &amp; <code>useCallback</code> vs <code>computed</code>（最大的心智负担差异！）</h3><p>这是我认为 Vue 转 React <strong>最需要适应的核心区别</strong>：</p><ul><li><strong>在 Vue 3 中</strong>：组件的 <code>&lt;script setup&gt;</code> <strong>只在初始化时执行一次</strong>。<code>computed</code> 自动收集依赖，数据变了才重新计算，完全不需要手动管理函数的引用。</li><li><strong>在 React 中</strong>：<strong>每次状态变化，整个函数组件就会从头到尾重新执行一遍！</strong></li><li>组件内的普通函数每次渲染都会重新声明新的引用，导致传递给子组件时引发子组件无意义的重渲染；</li><li>因此必须显式使用 <strong><code>useCallback</code></strong> 缓存函数引用，使用 <strong><code>useMemo</code></strong> 缓存计算密集型结果！</li></ul><p>好在后面的Next中已经可以开启Rect Compiler减少这种心智负担, 不然真的会死人的😢</p><h3 id="-useref-">④ <code>useRef</code> 的双重身份</h3><p>在 Vue 中 <code>ref</code> 既用来做响应式数据，又用来绑定 DOM。<br/>在 React 中：</p><ol start="1"><li>获取 DOM 节点：<code>&lt;div ref={domRef} /&gt;</code>；</li><li><strong>保存跨渲染周期的可变数据</strong>：<code>useRef(initialValue)</code> 改变 <code>.current</code> <strong>不会触发组件重新渲染</strong>，<code>.current</code>可以得到绑定的DOM的值</li></ol><h3 id="--hooksusetransition--usedeferredvalue">⑤ 并发特性 Hooks（<code>useTransition</code> &amp; <code>useDeferredValue</code>）</h3><p>文档中还深入到了 React 18 的并发特性：</p><ul><li><code>useTransition</code>：把某些不紧急的状态更新（如复杂的列表过滤）标记为低优先级过渡任务，防止输入框打字卡顿。</li><li><code>useDeferredValue</code>：延迟派生某个值的更新。<br/>这展示了 React 面对重型计算和交互优先级调度时的强悍能力。</li></ul><hr/><h2 id="7-react-router--zustand">7. 路由与全局状态管理：React Router 与 Zustand</h2><h3 id="-react-router-v7-vs-vue-router">① 路由篇：React Router v7 vs Vue Router</h3><ul><li><strong>Vue Router</strong>：中心化路由配置，配合内置的 <code>&lt;router-view&gt;</code>，导航守卫（<code>beforeEach</code>）非常直观。</li><li>React Router (v7)：有loader和action两个路由的行为, 这个是跟Vue有很大区别的地方, 可以在页面上要Get请求获取数据的地方提前在路由的loader中执行, 页面中需要执行Post提交表单的地方执行路由action里面的逻辑, 提交表单数据, 然后回到页面还能自动执行一次loader获取一次新的数据, 这一点让开发的体感很好, 还有各种Hooks <code>useNavigate(), useParams(), useLoaderData(), useActionData()。</code></li><li>React Router还有很多模式, 但是实际上感觉还是使用跟Vue一样
的数据路由模式更好理解。</li></ul><h3 id="-zustand-vs-pinia">② 状态管理篇：Zustand vs Pinia</h3><p>文档在全局状态管理中选择了极其轻巧优雅的 <strong>Zustand</strong>，而不是笨重的传统 Redux：</p><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">import { create } from &#x27;zustand&#x27;

const useStore = create((set) =&gt; ({
  bears: 0,
  increase: () =&gt; set((state) =&gt; ({ bears: state.bears + 1 })),
}))</code></pre><ul><li>对比 Vue 的 <strong>Pinia</strong>，两者的开发体验惊人地相似：都摒弃了 Mutation 的概念，体积轻巧，支持直接解构 Hook 使用。</li><li>但 Zustand 在内部依然严格遵循 React 的不可变合并思想，写起来非常纯粹。</li><li>
Zustand 还有自定义各种middleware和官方推荐的第三方的middleware就比如Immer, 这个就很好使用, 能不用管React什么傻x的不可变数据原则, 虽然底层还是保留了这个原则。</li></ul><hr/><h2 id="">总结：我的学习感想</h2><p>学完这整套 React 文档，如果用一句话总结两者的区别：</p><blockquote><p><strong>Vue 像一辆配置自动档位的车</strong>：响应式自动追踪、模板指令丰富、Scoped CSS 开箱即用，框架替你处理好了绝大多数优化细节，上手平滑、开发体验极度舒适。 就很自动， 而且三剑客不分离，代码美观易懂 ，新手上手很快，跟着官方走准没错，生态方面差一点。</p><p><strong>React 则像一辆纯手动挡的车</strong>：没有魔法，纯粹靠原汁原味的 JavaScript 驱动。它的单向数据流、不可变思想和 Fiber 架构非常严谨，但也正因如此，很多优化得自己去调用Hooks。三剑客合家，代码不美观。但是React的生态丰富， 社区热情度更高，有很多UI库和Hooks库，人人都可以处理React麻烦的点，欣欣向荣。</p></blockquote>
<p>作为刚刚学完理论知识、尚未经历大型项目实战的前端学习者，我不知道这些说的对不对，但是这就是我个人的感受。学习React我认为在如今是必须，无论是你为了ReactNative的跨端带来的便利，还是为了阅读更多React，Next的优秀项目，React都是作为前端的必需品。在Ai时代下，学习一个框架变得更加的简单，不懂的知识能问Ai快速的获取，代码也从一定要能写出来，更加的倾向于能看懂，知道逻辑，技术栈的选择感觉也更偏向于Ai能不能把这个技术的代码给写好，毕竟现在都是Ai写代码。</p></div><p style="text-align:right"><a href="https://sin6626.me/posts/study&amp;think/react-vs-vue-learning-experience#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://sin6626.me/posts/study&amp;think/react-vs-vue-learning-experience</link><guid isPermaLink="true">https://sin6626.me/posts/study&amp;think/react-vs-vue-learning-experience</guid><dc:creator><![CDATA[Sin]]></dc:creator><pubDate>Tue, 01 Sep 2026 14:20:43 GMT</pubDate></item><item><title><![CDATA[前端已死&世事无常]]></title><description><![CDATA[<link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/bfdn3s57ks38o04e42.png"/><link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/rzkg40mpfxjppyv360.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://sin6626.me/notes/9">https://sin6626.me/notes/9</a></blockquote><div><blockquote><p> 上班疲倦，前端已死。为什么世事无常，明明昨日还在一起工作，一起吃饭，一起讨论，一起玩乐的人，今天却要离开公司</p></blockquote>
<h2 id="">纯写日记，把我的感受写下来</h2><p> 我很难受这一周，公司很压抑，工作压力很大，心情很不好，更加难受复杂的是，又有一个实习生同事要走了，是公司想让他走，这让我更加的难受。原本的四人，然后三人，到了现在的两人，昨日还见的人，次日就再也见不到。</p><p>这次走的后端兄弟是跟我聊天最多的人，他叫hanirz，是我在公司第一个见到的人，我当初第一天上班，没有录制指纹，早早到的了公司了门口，但是进不去。他是随后到了公司，是我见到的第一个公司的同事，他开了门，我们交谈，聊着公司怎么样，谁谁谁是怎么样的，我的座位在哪，还告诉了我WiFi密码。我记得那天，是周五，上完那天就周末放假，第一天上班，Mentor好像也没想让我干活，就让我去看UniApp的文档，把文档都过一遍。我就看了一上午文档。中午12点吃饭，我不知道吃什么，只是记得技术群里面的PP告诉过我，中午吃饭跟同事们一起去吃。这时候hanirz突然拍我的肩膀说要不要一起去吃饭，我很高兴，就跟一起实习生们去吃饭了，一共四人，他们都是大三的。</p><p>午饭后，我跟他们说要不要先不要上去，反正上去也是睡觉，干脆在下面多聊天会，都介绍认识下，那时候都还是很热血的实习生，第一次实习，都在讨论Java，项目又是怎么怎么的，哪里有怎么了。hanirz他就在抱怨，Mentor给了他一个项目，让他去测试项目流程，有没有bug什么的，然后去修，但是他是一个Java后端，他不懂前端的代码，不知道前端传递的参数是什么，怎么看，说他搞得他很难受，于是我说让我去搞那个项目的前端，配合他去测试和修复bug，他也很高兴，于是我们就第一次搭把手配合了起来。后面随着越来越熟络，感觉四人的感情也越来越好，很好的同事关系。</p><h2 id="">前端有涯</h2><p>中途有个同为前端实习生，做他那个项目天天加班，并且客户要求很多，小而多，还要跟同为项目经理的后端去对接口，有时候晚上7,8点了还要改页面，还要打包给客户看。有人晚上项目经理diao了，然后回了一句，第二天就提出了离职，他说他受不了了。</p><p><img src="https://s3.sin6626.me/mx-images/bfdn3s57ks38o04e42.png" alt="image.png"/></p><p>今天还有一个正式工的前端女生，也被diao了，被说得还掉眼泪了，她是一个工期很长的项目快要到交付的时候还有很多功能没有实现，而且页面上的图还不是真实的切图而是蓝湖上面的网络图片。说实话这点确实是她的问题，而且是很愚蠢的问题。然后晚上她回去还要在改页面，核对接口，说实话真的心痛，看着前端兄弟们都是当牛做马，我很痛苦。我自己和后端联调的时候，后端调用接口不自己去Postman，ApiFox调用调试，让前端自己调用，让前端把接口返回发出来，Api文档信息不够，Ai自己猜接口返回值含义，然后从上级那听着客户要求，让Ai去改页面，写着屎山代码。</p><p>我看着他们天天改页面，然后为了项目还要天天加班，我心很累，虽然网络上可能说做互联网这一行，加班是常态，但是我认识的学前端的人都不加班，只有一个做上海做芯片的自愿加班，因为加班费很多。我向往的前端是一个很有创意力的职业，AnthonyFu的耗牛毛地图，面向技术人员做PPT的slidev和各种各样的动画效果；Innei的MixSpace生态，Yohaku和Shiro；青山老哥的VueMini；洛鱼和NeKo等人的airi。在我看来前端充满着各种创意，还有对于美的感受，所以我看见有前端兄弟为了改页面，为了调接口而加班到很晚，让我很难过，很窒息，这还是我们行业吗，为什么前端天天就是用着Ai去改页面，跟着设计稿拿切图贴在页面上，改着客户无理取闹的要求，还要被老板压力。网络上都在说前端已死，没工作的时候我会理解为Ai很强大了，能够代替前端直接完成页面。但是实际上呢，公司使用Uniapp，Ai对Uni不够熟悉，开发工具链没有Web完善，Ai难以调试，有的前端兄弟天天加班，被老板压力，真正的前端已死，我想我可能才明白。不是说前端已经可以被Ai替代，而是小前端的未来让人绝望的想死，难怪之前技术群里面有人让我不要去学前端，真的坐牢。</p><h2 id="">世事难料</h2><p>说回hanirz，其实我并不知道公司想让他走明确的原因和导火索，根据他说，是因为他有要5天完成的项目，他第一天为了不浪费即将刷新的codex的额度，于是开5.6 Sol 写了四个小时代码，然后加上第二天的代码，分几次提交100多个文件。然后就被经理叫过去说了他写的代码怎么怎么样，说多做了什么什么业务，什么错误枚举类放在了Service层什么的，我并是不很了解。在我眼里只是，我下午在看着VueQuery，做着手里的活，hanirz被经理喊过去了一次，然后下午下班要去回去的时候，突然hanirz跟我说自己要走。那时候我还以为他开玩笑呢，问了他好几遍，回去的时候他们就把下午发生的事情都说了出来。他下午还被Mentor喊过去了，问他要不要，跟他说了工资的事情。</p><p>谁能想到，昨天还一起吃饭，一起玩乐，说好了10月份一起走的人，今天突然告诉你他要走了。在压力最大的时候，不是被压力走了，而是公司暗示让你走，苦涩复杂。我第一次实习，在公司第一个认识的人，玩的好的朋友，就这样不给我反应的时间下走了。下班后我们三人去吃了一餐散伙饭，饭桌上说我们说了很多很多事，但是都没什么用。</p><p><img src="https://s3.sin6626.me/mx-images/rzkg40mpfxjppyv360.png" alt="合影.png"/></p><p>hanirz的走其实让我也不得不思考最近的打算，原本是打算10月份走，上三个月班，抽空学习，完成自己上班前立的三个Flag，确实自己这段时间抽空学了很多，NuxtUI，NuxtContent，i18n，Nuxt Studio，Iconify图标，精进Tailwind，Gsap，MorphSVG插件，Motion-V，View Transition API，ClipPath属性 stroke-dasharray + stroke-dashoffset构造CVG简笔画，Canvas，Web Audio Api，Threejs，WebGL，OpenGL，Shader，GLSL，VueQuery，Vercel使用，也做出了自己喜欢的东西，公司也让我学到了很多Android方面的知识，NotificationListenerService通知栏监听，ContentResolver去读取系统短信数据库，还有很多UniApp中的小坑小点小细节。hanirz的离开让我觉得我可能也不一定能在这里待到10月了，我之前立的三个Flag可能还真的无法完成。</p><p>前端让我又爱又恨，hanirz的离开我伤心复杂，但是对他来说可能也是一种解脱，我还有一年的时间，还有机会，稳住节奏，<ins><em>向着自己的热爱前进，厚积而薄发，谋定而后动</em></ins></p></div><p style="text-align:right"><a href="https://sin6626.me/notes/9#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://sin6626.me/notes/9</link><guid isPermaLink="true">https://sin6626.me/notes/9</guid><dc:creator><![CDATA[Sin]]></dc:creator><pubDate>Fri, 21 Aug 2026 16:50:21 GMT</pubDate></item><item><title><![CDATA[【FRP + Vite 踩坑记录】localhost 能访问，公网却 502？排查 IPv6、服务重载与代理重名]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://sin6626.me/posts/crash/frp-vite-localhost-502-public-ipv6-reload-proxy">https://sin6626.me/posts/crash/frp-vite-localhost-502-public-ipv6-reload-proxy</a></blockquote><div><blockquote><p>踩坑日记,  由Ai总结  ---- 踩过的坑才印象深刻, 才能真正记住</p></blockquote>
<hr/><h2 id="-1localhost-127001--502">坑 1：localhost 能通，127.0.0.1 却连不上（导致公网 502）</h2><h3 id="1-">1. 现象与排查</h3><p>本地 Vite 服务运行在 <code>3200</code> 端口，浏览器访问 <code>http://localhost:3200</code> 正常，但执行 <code>curl.exe http://127.0.0.1:3200</code> 却报错 <code>Failed to connect: Could not connect to server</code>。</p><p>检查端口监听状态：</p><pre class=""><code class="">powershellnetstat -ano | findstr :3200
# 输出：TCP    [::1]:3200    LISTENING</code></pre><h3 id="2-">2. 根因分析</h3><ul><li><strong><code>localhost</code></strong>：现代系统与 Node.js 17+ 默认优先解析为 IPv6 的 <code>[::1]</code>。</li><li><strong><code>127.0.0.1</code></strong>：是 IPv4 回环地址。Vite 默认只绑定了 IPv6 <code>[::1]</code>，IPv4 根本没有服务监听。</li><li><strong>连锁反应</strong>：<code>frpc.toml</code> 中配置了 <code>localIP = &quot;127.0.0.1&quot;</code>，FRP 转发公网请求到本地 <code>127.0.0.1:3200</code> 被拒，对外直接表现为 <strong>HTTP 502</strong>。</li></ul><table><thead><tr><th>访问方式</th><th>对应协议</th><th>监听状态</th><th>访问结果</th></tr></thead><tbody><tr><td><a href="http://localhost:3200">http://localhost:3200</a></td><td>优先 IPv6 (::1)</td><td>已监听</td><td>✅ 正常</td></tr><tr><td><a href="http://[::1]:3200">http://[::1]:3200</a></td><td>纯 IPv6 回环</td><td>已监听</td><td>✅ 正常</td></tr><tr><td><a href="http://127.0.0.1:3200">http://127.0.0.1:3200</a></td><td>纯 IPv4 回环</td><td>未监听</td><td>❌ 连接被拒</td></tr></tbody></table><h3 id="3-">3. 解决办法</h3><p>修改 <code>vite.config.ts</code>，显式配置 <code>host: &#x27;0.0.0.0&#x27;</code>（监听全网卡双栈）：</p><pre class=""><code class="">tsexport default defineConfig({
  server: {
    host: &#x27;0.0.0.0&#x27;,
    port: 3200,
  },
})</code></pre><hr/><h2 id="-2-frpstoml">坑 2：修改了 frps.toml，端口却未生效</h2><h3 id="1-">1. 现象与根因</h3><p>服务端修改配置 <code>bindPort = 7100</code> 后执行了 <code>systemctl start frps</code>，客户端仍报错 <code>session shutdown</code>。</p><ul><li><strong>原因</strong>：<code>systemctl start</code> 只负责启动未运行的服务。<code>frps</code> 已经在后台常驻运行了数月，执行 <code>start</code> 不会重新加载配置。</li></ul><h3 id="2-">2. 解决办法</h3><pre class=""><code class="">bashsudo systemctl restart frps       # 重启以加载新配置
ss -lntp | grep 7100              # 确认 7100 端口已处于 LISTEN 状态</code></pre><hr/><h2 id="-3proxies">坑 3：<code>[[proxies]]</code> 代理配置名称重复</h2><h3 id="1-">1. 现象与根因</h3><p><code>frpc.toml</code> 中配置了多个代理节，但 <code>name</code> 字段使用了相同名称：</p><pre class=""><code class="">toml# ❌ 错误：两处代理 name 重复
[[proxies]]
name = &quot;mqtt_remote&quot;
localPort = 1883
remotePort = 6183
[[proxies]]
name = &quot;mqtt_remote&quot;
localPort = 3200
remotePort = 6001</code></pre><p>客户端日志报错：<code>start error: proxy [mqtt_remote] already exists</code>。在 FRP 中，<code>name</code> 是每个 Proxy 唯一的标识 ID。</p><h3 id="2-">2. 解决办法</h3><p>确保每个代理条目的 <code>name</code> 全局唯一：</p><pre class=""><code class="">toml# ✅ 正确配置
[[proxies]]
name = &quot;mqtt_remote&quot;
type = &quot;tcp&quot;
localIP = &quot;127.0.0.1&quot;
localPort = 1883
remotePort = 6183
[[proxies]]
name = &quot;web_3200&quot;
type = &quot;tcp&quot;
localIP = &quot;127.0.0.1&quot;
localPort = 3200
remotePort = 6001</code></pre><hr/><h2 id="-">🛠️ 标准排查链路速查</h2><pre class=""><code class="">text公网请求 (38.95.74.213:6001) ──&gt; frps 服务端 ──&gt; frpc 客户端 ──&gt; 本地服务 (127.0.0.1:3200)</code></pre><p>遇到穿透异常，建议按 <strong>“服务端 -&gt; 本地服务 -&gt; 网络连通 -&gt; 日志”</strong> 的四步链路排查：</p><ol start="1"><li><strong>检查服务端监听</strong>：<code>ss -lntp | grep frps</code>（确认 <code>7100</code> 通信端口与映射端口处于 LISTEN 状态）。</li><li><strong>测试本地服务可用性</strong>：powershellTest-NetConnection 127.0.0.1 -Port 3200
Test-NetConnection 127.0.0.1 -Port 1883<em>(如果本地 IPv4 都不通，FRP 必定返回 502，先改 Vite&amp;#160;<code>0.0.0.0</code>)</em></li><li><strong>测试公网端口连通</strong>：powershellTest-NetConnection 38.95.74.213 -Port 6001</li><li><strong>定位客户端/服务端日志</strong>：查看 <code>frpc.log</code> 或 <code>frps.log</code> 是否存在 <code>proxy already exists</code> 或认证报错。</li></ol><hr/><h2 id="-">📌 核心经验总结</h2><ol start="1"><li><strong><code>localhost</code>&amp;#160;≠&amp;#160;<code>127.0.0.1</code></strong>：遇连接异常先跑 <code>netstat -ano | findstr :端口</code> 确认是 <code>[::1]</code> 还是 <code>127.0.0.1</code> / <code>0.0.0.0</code>。</li><li><strong>改配置务必&amp;#160;<code>restart</code></strong>：<code>systemctl start</code> 不会重载正在运行的进程。</li><li><strong><code>[[proxies]]</code>&amp;#160;必须命名唯一</strong>：<code>name</code> 是 FRP 的唯一代理 ID。</li></ol></div><p style="text-align:right"><a href="https://sin6626.me/posts/crash/frp-vite-localhost-502-public-ipv6-reload-proxy#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://sin6626.me/posts/crash/frp-vite-localhost-502-public-ipv6-reload-proxy</link><guid isPermaLink="true">https://sin6626.me/posts/crash/frp-vite-localhost-502-public-ipv6-reload-proxy</guid><dc:creator><![CDATA[Sin]]></dc:creator><pubDate>Tue, 18 Aug 2026 14:40:34 GMT</pubDate></item><item><title><![CDATA[塞翁失马，焉知非福]]></title><description><![CDATA[<link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/xnzx0c5angkk0hmdbh.png"/><link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/vnyzpnofl8nm8qrdbm.png"/><link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/8uifq8h287jt7mabhc.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://sin6626.me/notes/8">https://sin6626.me/notes/8</a></blockquote><div><p>这两天发生的事情让我深刻体会到了这句谚语 ---- “<ins><em>塞翁失马，焉知非福</em></ins>”</p><h2 id="812">8月12号</h2><p>隔壁Agent交流群里面有人在抱怨codex速度太慢了，额度烧得太快了，然后就考虑要投奔Grok。他有一个比价的网站，里面有很多的codex和Grok的代充。于是他在网站上找到一家店铺，花费330R拿下Grok Heavy（一个月300美刀）的三个月的订阅。。。</p><p>那会也是刚出了Grok的imagine2，而且听他说生图能力可以比肩上GPT-Image-2，中文的排版能力也不差。加上前一阵子出的Grok4.5和Grok Build， 这下可以说Grok真的能够上桌了。而且Grok 在11号还推出了他们的GrokBot（这个用起来体感真的好），听说之前是xAi内部人员使用的。现在Grok Heavy可以直接使用GrokBot，并且还会送一个月的Cursor Ultra（一个月200美刀），简直无敌！！！。而这位群友仅仅只花了330R， 我心动了🩷，学着他用了黑充。第二天就后悔了。</p><h2 id="">第二天</h2><p>第一天晚上我就开始捣鼓起来了这些新的Ai工具（对我而言）， 下载了Cursor，GrokBuild， Grok App， GrokBot。</p><p>第二天到公司我就很高兴跟实习生同事们说这些工具的事情，那可是顶档的Cursor，顶档的Grok，Cursor能用fable 5，Grok Build 能一直开 Grok 4.6 High（凌晨出了4.6）， 额度都不带掉的。加上公司老板们都没喊我干活，我能一直研究这些新工具的使用，尤其是GrokBot，用起来真的顶。相当于0配置的Hermes，并且产品有顶级前端UI，就这个UI，那位前端看了能不爱？于是我一上午就都在使用GrokBot。</p><p><img src="https://s3.sin6626.me/mx-images/xnzx0c5angkk0hmdbh.png" alt="image.png"/></p><p>但是呢，中午醒来我直接傻眼了，隔壁群里面黑充GrokHeavy的兄弟的订阅掉了！我也赶紧打开电脑看下我的Grok订阅，果不其然也掉了。24小时都没到，黑充都掉了，只有小部分幸存者。我的330大洋啊！一天都没到，直接掉，黑充再也不充了！</p><p>但是也有一个好消息，到目前为止Cursor Ultra没有掉，而且我的GrokBot也能跑。（希望明天还能跑。。。）</p><h2 id="follow-grokbot">Follow GrokBot</h2><p>GrokBot这个产品真的是我从今年的opencode开始，使用过最好的Ai工具了，真的惊艳到我了，体感无敌好。一句话形容：“<ins><em>0配置，开箱即用的SuperHermes</em></ins>”。GrokBot中的每个Bot都共享一台云电脑，但是每个Bot都有一个电脑的界面（大概，没有专门去连去看），电脑里面初始只有三个东西，一个谷歌浏览器，一个文件管理器，一个终端，你能连接直接连接他们电脑去看，去使用（类比ToDesk）</p><p><img src="https://s3.sin6626.me/mx-images/vnyzpnofl8nm8qrdbm.png" alt="image.png"/></p><p>他的产品就像是一个微信的聊天界面，你的联系人就是你创建的各个Bot，既然像个微信，那么肯定是有群聊功能，也可以像微信一样使用@符号去专门的指定Bot。你只需要在群里面说话，说一个大概要做的事情，分配好任务的Bot们就会去他们的电脑里面去执行任务。并且他们能像同事一样群内互相@通信，共享资料，同事之前私聊也可。例如这样：</p><p><img src="https://s3.sin6626.me/mx-images/8uifq8h287jt7mabhc.png" alt="image.png"/></p><p>还有很多的很多的功能，比如routine，teach a task等。我特意去他的dashboard上看了下他模型的调用情况，模型大部分都是使用的Grok4.6，还有的时候会使用opus5，都是目前最顶级的大模型。并且额度很耐用，跑了一天的GrokBot，周额度才用21%。今天新开的小说是他们做的，从写作到发布全程没动收，全是他们做的。他们一天都在写作，先本地写好md文件，记忆文件，人物文件等等，然后去使用虚拟鼠标在他们的电脑里面上传草稿到我的番茄小说作家助手后台，然后经过我的同意去发布小说。要是经历过Window无法使用codex 的Computer Use的痛苦就懂了。在codex上跑这套流程，不知道要踩坑多少次，烧多少额度，花多少时间（codex龟速）。</p><h2 id="">额外的话</h2><p>其实本来是想着第一次拿到工资的时候写一篇手记的，但是发工资的时间跟本篇的说的事情重叠了，由于GrokBot实在是太好用了，我还是决定写GrokBot，虽然可能没写出GrokBot到底有多强大。亏损了330大洋， 换来了不是我刚需的最新的Ai产品的使用权，我不知道是福还是祸，只能说“<em>塞翁失马，焉知非福</em>”</p></div><p style="text-align:right"><a href="https://sin6626.me/notes/8#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://sin6626.me/notes/8</link><guid isPermaLink="true">https://sin6626.me/notes/8</guid><dc:creator><![CDATA[Sin]]></dc:creator><pubDate>Thu, 13 Aug 2026 16:19:21 GMT</pubDate></item><item><title><![CDATA[gsap官方文档的更新和Svg线条动画的探索]]></title><description><![CDATA[<link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/7gweo0be99fwashiid.png"/><link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/qczwmkuxyisv4ct0gd.png"/><link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/kookx4atq5600mm5u5.png"/><link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/46olpsd7t2vrg09ff2.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://sin6626.me/notes/7">https://sin6626.me/notes/7</a></blockquote><div><p>想想这篇文章应该是要发到博文才对的，但是感觉应该更多的去诉说出我的心情与感受，所以还是放在手记里吧</p><h2 id="">两件事</h2><p>主要想要记录两件事情, 第一件事情让我很激动, 从早上激动到下午, 因为我上周提出的有关gsap官方文档的issue建议被官方采纳了! 今天一早上起来就看见gsap官方给我发的邮件了. 第二件事情让我感觉很有意义, 因为我发现了我平时所研究的东西在3年前也有前辈跟我研究一样的东西.</p><h2 id="issue">第一次提出issue建议被采纳</h2><p>睡醒查看手机信息的时候, 我惊喜的发现有两封邮箱, 标题赫然写着&quot;<ins><em>Re: [greensock/GSAP] Docs: clarify Vue refs in gsap.context() scope examples (Issue #652</em></ins>)&quot;. 内容是:</p><p><img src="https://s3.sin6626.me/mx-images/7gweo0be99fwashiid.png" alt="image.png"/></p><p>我迫不及待的就把信息发给我了我的同事和技术群的兄弟们, 因为太激动了啊, 第一次提出issue, 第一次被官方回应.虽然不是PR被合并, 但是也很高兴了.</p><p>回顾下上周五:</p><p>昨天项目的UI还没设计出来, 所以依旧是带薪学习的一天, 我把我的UI&amp;Animation项目(还在开发中)的动画重构加收藏之后, 就继续开始了gsap文档的阅读, 主要是看入门gsap方面的文档, 看完后, 去找了一个使用了Nuxt4 + gsap的<a href="https://github.com/BenjaminOddou/portfolio">开源项目</a>,  想去看看别人是如何在Vue使用gsap的. 我在项目中看见gsap.content() 的时候的时候就很疑惑, 因为gsap官网文档哪里, 总是说什么ref, 但是都是对于React和Angular的ref, 没有对于Vue的ref的说明. 但是我看项目使用的时候明明不是用的ref对象, 是把ref对象给.value取出了真正的dom元素的.</p><p>于是就跟技术群里的兄弟们说gsap官方偏心, 只给React和Angular说明, 没有给Vue一点情报, 文档太容易误解了. 没想到群内的Guoxk大捞直接让我提一个PR去, 这可给我吓到了, 我压根都想过这种事情.</p><p><img src="https://s3.sin6626.me/mx-images/qczwmkuxyisv4ct0gd.png" alt="image.png"/></p><p>当然还有群内的糖糖</p><p><img src="https://s3.sin6626.me/mx-images/kookx4atq5600mm5u5.png" alt="image.png"/></p><p>因为github上gsap官方仓库上没有记录文档的内容, 所以提PR也不会被合并, 于是我就提了一个issue</p><p><img src="https://s3.sin6626.me/mx-images/46olpsd7t2vrg09ff2.png" alt="image.png"/></p><p>然后就是到今天的回复了.</p><h2 id="svg">今天下午有关Svg的研究</h2><p>今天下午继续研究BenjaminOddou的portfolio项目, 也是收集到了很多很有用的动画, 就有Svg线条动画.</p><p>这个动画让我感觉很有意义, 不是说这个动画有多么的实用(虽然可能真的很实用), 有多么的高深, 只是因为在3年前, Nuxt核心团队成员, unocss的创始人 ---- <a href="https://antfu.me/"><em>Anthony Fu</em></a>(一个托尼) 大佬也曾研究过这个动画, 我喜欢的大佬曾经也研究过的动画.这个消息还是我今天下午问了群内的Guoxk大捞有关Svg图片制作才知道的. 托尼老师发的博客 -&gt; <a href="https://antfu.me/posts/animated-svg-logo">To Blog</a> (动画详细博客中有, 就不解释了)</p><p>这件事情为什么也想记录下呢, 感觉很单纯, 单纯因为前端的前辈大佬们曾经也经历这段学习, 就让人感觉自己的研究是有价值的. 平时有关内容驱动式网站, 组件, 动画等等的学习, 我觉得真的很有意义, 现在可能没有使用上很多次, 也用的很不熟练, 没有那种实用性上的意义, 但是这份技术的宝藏会一直在这里, 待人来发掘.</p><blockquote><p><ins><em>三年前的月，也曾照过那些探索 SVG 动画的人；今日我所见之光，亦是他们曾追寻过的光。</em></ins></p></blockquote></div><p style="text-align:right"><a href="https://sin6626.me/notes/7#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://sin6626.me/notes/7</link><guid isPermaLink="true">https://sin6626.me/notes/7</guid><dc:creator><![CDATA[Sin]]></dc:creator><pubDate>Tue, 28 Jul 2026 14:59:32 GMT</pubDate></item><item><title><![CDATA[NestJS学习]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://sin6626.me/posts/study&amp;think/nestjs-learning">https://sin6626.me/posts/study&amp;think/nestjs-learning</a></blockquote><div><h2 id="">前言</h2><p>最近花时间学了 NestJS，从一个「听说过但没碰过」的状态，到能搭出一个完整的后端项目。这篇东西算是一个学习记录，把过程中的理解、踩过的坑、对框架的感悟写下来，也算是对自己这段时间的一个交代。</p><hr/><h2 id="-nestjs-">我对 NestJS 的理解</h2><p>刚接触的时候，最直观的感受是：<strong>它和传统的 Node.js 框架不太一样</strong>。</p><p>以前写 Express ，都是&quot;需要什么自己加&quot;，很灵活，但也容易写散。NestJS 给我的感觉是 —— 它把规矩定好了。模块怎么拆、代码放哪里、请求怎么流转，都有一套明确的规范。</p><p>刚开始觉得有点束缚，但写多了发现，<strong>这种约束其实是一种解放</strong>。项目结构不会随着代码量增加而腐烂，回头看自己写的代码，思路是清晰的。</p><hr/><h2 id="">项目结构：模块化是核心</h2><p>学 NestJS 最先要理解的就是模块化。每个功能都是一个模块，模块里放控制器、服务、数据实体，各司其职：</p><pre class=""><code class="">src/
├── main.ts             # 应用入口
├── app.module.ts       # 根模块，组装所有子模块
├── user/               # 用户模块
├── auth/               # 认证模块
├── roles/              # 角色模块
├── logs/               # 日志模块
├── guards/             # 自定义守卫
├── filters/            # 异常过滤器
├── interceptors/       # 拦截器
├── decorators/         # 自定义装饰器
├── enum/               # 常量枚举
└── utils/              # 工具函数</code></pre><p>每个模块就像一个小盒子，有自己的职责，也能和其他模块通信。这种组织方式让项目不管多大，结构都是可预期的。</p><p>如果一个模块需要被其他多个模块复用，可以把它设为全局模块（<code>@Global()</code>），或者通过 <code>exports</code> 显式暴露出去。这种显式的依赖声明，比隐式的全局变量要健康得多。</p><hr/><h2 id="">学到最多的几个点</h2><h3 id="1-di">1. 依赖注入（DI）</h3><p>NestJS 的依赖注入是我觉得最优雅的设计之一。不需要自己 new 对象，只需要在构造函数里声明需要什么，框架会自动给你注入：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">constructor(private userService: UserService) {}</code></pre><p>就一行，<code>UserService</code> 的实例就进来了。如果 <code>UserService</code> 又依赖了其他东西，框架会递归地帮你全部解析好。</p><p>这种机制带来的好处是：<strong>模块之间的耦合度低，测试也方便</strong> —— 测试的时候可以轻松替换成 mock 对象。</p><h3 id="2-">2. 装饰器驱动的编程方式</h3><p>NestJS 大量使用装饰器，开始的时候会觉得&quot;到处都是 @ 符号&quot;，看习惯了会发现这是它最方便的地方。</p><p>路由、参数提取、验证、权限控制……全都是装饰器。一个路由的处理函数，从上到下的装饰器就是它的完整&quot;说明书&quot;：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">@Get(&#x27;:id&#x27;)
@UseGuards(AuthGuard)
@UsePipes(ValidationPipe)
getUser(@Param(&#x27;id&#x27;) id: string) { ... }</code></pre><p>一眼就能看出来：这是个 GET 请求、需要登录、参数验证、从路径拿 id。</p><p>除了用内置装饰器，还可以<strong>自定义装饰器</strong>，把重复的逻辑封装起来。比如我写了一个 <code>@Serialize(Dto)</code> 装饰器，用来控制接口返回哪些字段，敏感字段不想暴露给前端，通过这个装饰器一键搞定。</p><h3 id="3-">3. 请求生命周期（完整流水线）</h3><p>NestJS 的请求处理是一条完整的流水线，按顺序经过各个环节：</p><p><strong>Middleware → Guard → Interceptor → Pipe → Controller → Service → Interceptor → Filter（出异常时）</strong></p><p>每个环节各司其职：</p><ul><li><strong>Middleware</strong>：最前置的处理，可以做一些通用逻辑，比如请求日志、耗时统计</li><li><strong>Guard</strong>：能不能进来？做权限校验，比如检查是否登录、是否有操作权限</li><li><strong>Interceptor</strong>：进来前/出去后做点什么，比如打日志、加缓存、序列化响应数据</li><li><strong>Pipe</strong>：进来的数据对不对？做参数验证和类型转换</li><li><strong>Filter</strong>：出错了怎么返回？统一异常响应格式</li></ul><p>理解了这条流水线，写代码的时候就能清楚当前逻辑该放在哪个环节，出 bug 也能快速定位。</p><h3 id="4-typeorm-">4. TypeORM 的数据库操作</h3><p>数据库用了 TypeORM，最大的感受是：<strong>操作数据库像操作对象一样自然</strong>。</p><p>最常用的几种关系：</p><ul><li><strong>一对一</strong>：用户 ↔ 资料（一个用户只有一个资料页）</li><li><strong>一对多</strong>：用户 ↔ 日志（一个用户有多条操作记录）</li><li><strong>多对多</strong>：用户 ↔ 角色（一个用户可以有多个角色，一个角色也可以有多个用户，通过中间表关联）</li></ul><p>常规的增删改查直接用 Repository 的方法就行。复杂查询用 QueryBuilder，写起来和写 SQL 差不多但更安全（参数化查询），支持分组、排序、分页等高级操作。</p><h3 id="5-">5. 数据验证与转换</h3><p><strong>验证</strong>：以前写接口的时候，参数校验经常是手写 if-else，又臭又长。NestJS 搭配 <code>class-validator</code>，用装饰器声明规则，简洁很多：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">@IsString()
@IsNotEmpty()
@Length(2, 20)
username: string;</code></pre><p>配合全局注册的 ValidationPipe，请求进来的时候自动校验，不合法直接返回 400，Controller 里拿到的数据一定是合法的。</p><p><strong>转换</strong>：用 <code>class-transformer</code> 配合拦截器，可以控制接口返回的数据结构。比如把 User 实体转成 UserDto，只暴露前端需要的字段，密码、加密盐这些敏感字段自动过滤掉。这在写 API 的时候非常实用。</p><h3 id="6--jwt">6. 认证与授权 (JWT)</h3><p>用户认证用的是 JWT（JSON Web Token）方案。流程大概是：</p><ol start="1"><li>用户登录，服务端验证用户名密码</li><li>验证通过，生成一个加密的 token 返回给前端</li><li>前端后续请求带上这个 token</li><li>服务端通过 Guards 校验 token，确定用户身份</li></ol><p>密码不能明文存数据库，我用了第三方的包做加盐哈希处理（bcrypt），就算数据库泄露了，密码也是安全的。</p><p>权限控制方面还学了 CASL 这个库，可以定义细粒度的权限规则，比如&quot;普通用户只能看自己的文章，管理员可以删任何文章&quot;，配合自定义守卫来实现。</p><h3 id="7-">7. 环境配置管理</h3><p>用 <code>@nestjs/config</code> 配合 <code>.env</code> 文件做环境配置。不同环境用不同的文件：</p><pre class=""><code class="">.env.development    # 开发环境
.env.production     # 生产环境
.env.test           # 测试环境
.env                # 公共默认配置</code></pre><p>数据库连接信息、JWT 密钥、端口号这些都放在环境变量里，不写死在代码中。还用了 Joi 做配置验证，启动时如果缺少必要的配置项，程序会直接报错，避免带着错误配置上线。</p><h3 id="8-">8. 日志系统</h3><p>用 <code>nestjs-pino</code> 做日志记录，配置了双输出：</p><ul><li><strong>控制台</strong>：彩色格式化输出，开发时看着清晰</li><li><strong>文件</strong>：按天轮转，每个文件有大小限制，自动归档</li></ul><p>日志记录了每次请求的路径、方法、参数、耗时等信息，线上排查问题的时候很有帮助。</p><h3 id="9-jest">9. 测试（Jest）</h3><p>写了单元测试和端到端（e2e）测试。</p><p><strong>单元测试</strong>：针对 Service 和 Controller 单独测试，用 mock 替换数据库操作，跑得很快。主要测业务逻辑是否正确。</p><p><strong>端到端测试</strong>：模拟真实的 HTTP 请求，从路由到数据库全链路验证，确保接口整体功能正常。</p><p>用 Jest + Supertest 来写，虽然写测试确实多花一些时间，但改代码的时候有测试兜底，心里会踏实很多。</p><h3 id="10-">10. 异常过滤器</h3><p>没有异常过滤器的时候，每个接口出错返回的格式可能都不一样。前端对接起来很痛苦。</p><p>NestJS 的异常过滤器可以把异常处理收拢到一处，统一返回格式：</p><pre class="language-json lang-json"><code class="language-json lang-json">{
  &quot;code&quot;: 400,
  &quot;message&quot;: &quot;参数错误&quot;,
  &quot;timestamp&quot;: &quot;2026-07-26T00:00:00.000Z&quot;,
  &quot;path&quot;: &quot;/api/v1/user&quot;,
  &quot;method&quot;: &quot;POST&quot;
}</code></pre><p>不管是参数校验失败、数据库报错、还是业务逻辑异常，返回格式都是统一的。前端直接按统一格式处理就行。</p><hr/><h2 id="">写在最后</h2><p>NestJS 不是那种&quot;学完就能炫技&quot;的框架，它更像一个规规矩矩的框架，提供了写后端应用的一整套方法论。回头看我以前用 Express 写的项目，如果当初用 NestJS，结构应该会清晰很多。</p><p>这次学的东西挺多的：模块化架构、依赖注入、TypeORM 数据操作、JWT 认证、数据验证与转换、日志系统、单元测试和 e2e 测试、自定义装饰器和守卫、异常过滤、环境配置管理……每一样在生产中都是很实用的。</p><p>框架只是一个工具，但好的工具确实能帮人写出更干净的代码。但这次学习让我最大的收获不是记住了多少 API，而是理解了这套架构思维。未来写任何后端项目，这套思维方式都是通用的。</p><p>（其实在学的过程很难受，网上没有很好入门的NestJS课程，我看的这套很不适合小白入门，里面很绕，而且课程很老，不过老师水平确实高，好在硬着头皮学完了，但是说实话不一定记住了常见的API，因为自己代码量不够多。不过，后面肯定有要用到NestJS的时候，到那时候再提高吧）</p><hr/><blockquote><p>一步一个脚印，比什么都重要。</p></blockquote></div><p style="text-align:right"><a href="https://sin6626.me/posts/study&amp;think/nestjs-learning#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://sin6626.me/posts/study&amp;think/nestjs-learning</link><guid isPermaLink="true">https://sin6626.me/posts/study&amp;think/nestjs-learning</guid><dc:creator><![CDATA[Sin]]></dc:creator><pubDate>Sun, 26 Jul 2026 15:59:16 GMT</pubDate></item><item><title><![CDATA[Protfolio Template - Home Page - GLM的总结]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://sin6626.me/posts/source-code-study/portfolio-template-ai">https://sin6626.me/posts/source-code-study/portfolio-template-ai</a></blockquote><div><h1 id="tailwind--nuxt-ui-">Tailwind / Nuxt UI 经典套路速查</h1><blockquote><p>本项目开发过程中累积的高频、可直接复用的样式搭配速查表。
涵盖定位、毛玻璃浮岛、Tailwind 4 新语法、Nuxt UI 主题色别名、动效联动等。</p></blockquote>
<hr/><h2 id="--">一、定位 / 居中三件套</h2><h3 id="">任意宽度元素的绝对居中(最经典)</h3><pre class="language-html lang-html"><code class="language-html lang-html">&lt;div class=&quot;fixed left-1/2 -translate-x-1/2&quot;&gt;
  ...
&lt;/div&gt;</code></pre><ul><li><code>left-1/2</code>:把元素<strong>左上角点</strong>放到屏幕水平中线</li><li><code>-translate-x-1/2</code>:再回拉<strong>自身宽度的一半</strong>,实现真正的几何居中</li><li>不依赖具体 px,任意宽度都居中,比 <code>margin-left: -w/2</code> 优雅</li></ul><h3 id="--fab">顶部悬浮岛(常用于导航 / FAB)</h3><pre class="language-html lang-html"><code class="language-html lang-html">&lt;div class=&quot;fixed top-2 sm:top-4 left-1/2 -translate-x-1/2 z-10&quot;&gt;
  ...
&lt;/div&gt;</code></pre><p>参考本项目的 <code>app/components/AppHeader.vue</code>。</p><hr/><h2 id="macos-">二、毛玻璃浮岛(macOS 风)</h2><pre class="language-html lang-html"><code class="language-html lang-html">&lt;div class=&quot;
  bg-muted/80
  backdrop-blur-sm
  rounded-full
  border border-muted/50
  shadow-lg shadow-neutral-950/5
&quot;&gt;
  ...
&lt;/div&gt;</code></pre><p>拆解:</p><table><thead><tr><th>Class</th><th>含义</th></tr></thead><tbody><tr><td><code>bg-muted/80</code></td><td>Nuxt UI 主题色 <code>muted</code> 80% 透明,做模糊基底</td></tr><tr><td><code>backdrop-blur-sm</code></td><td>模糊背后内容,核心的「毛玻璃」质感</td></tr><tr><td><code>rounded-full</code></td><td>胶囊化(两端半圆)</td></tr><tr><td><code>border border-muted/50</code></td><td>同色更淡的描边,做精细分层</td></tr><tr><td><code>shadow-lg shadow-neutral-950/5</code></td><td>Tailwind 4 拆分写法,大小 + 颜色独立,亮色场景的<strong>淡阴影</strong>(5%的透明度)不突兀</td></tr></tbody></table><blockquote><p>适用:任何浮层、popover、浮动按钮、顶部导航胶囊。</p></blockquote>
<hr/><h2 id="tailwind-4-">三、Tailwind 4 新语法(高频三招)</h2><h3 id="1--css-">1️⃣ 任意 CSS 变量值</h3><pre class="language-html lang-html"><code class="language-html lang-html">&lt;div class=&quot;max-w-(--ui-container)&quot;&gt;</code></pre><p>等价于 <code>max-width: var(--ui-container)</code>,括号内可放任意 CSS variable,替代旧版 <code>max-w-[1400px]</code> 死值。</p><h3 id="2-data-attr-">2️⃣ data-attr 状态选择器</h3><pre class="language-html lang-html"><code class="language-html lang-html">&lt;!-- 本元素状态 --&gt;
&lt;button class=&quot;data-[state=active]:text-highlighted data-[state=inactive]:text-muted&quot;&gt;

&lt;!-- 父级挂 group 时,子元素响应父级 data-state --&gt;
&lt;div class=&quot;group&quot;&gt;
  &lt;Icon class=&quot;group-data-[state=open]:rotate-135&quot; /&gt;
&lt;/div&gt;</code></pre><p>配合 Reka UI / Nuxt UI 的 <code>data-state</code> 自动 toggle,<strong>纯 CSS 响应组件状态</strong>,不写 JS 切 class。</p><h3 id="3-important-">3️⃣ important 后缀</h3><pre class="language-html lang-html"><code class="language-html lang-html">&lt;div class=&quot;pt-0! px-0!&quot;&gt;</code></pre><p>Tailwind 4 把 <code>!</code> 移到末尾(旧版是 <code>!pt-0</code>)。<strong>压第三方库默认样式必备</strong>——比如压过 Nuxt UI 组件自带的 padding/ring。</p><hr/><h2 id="--">四、响应式出血 + 兼容器锁宽</h2><p>「让某区块出血到全宽,但又被 UI 容器限宽」的经典组合:</p><pre class="language-html lang-html"><code class="language-html lang-html">&lt;div class=&quot;-mx-4 sm:-mx-12 lg:-mx-16 max-w-(--ui-container)&quot;&gt;</code></pre><table><thead><tr><th>Class</th><th>含义</th></tr></thead><tbody><tr><td><code>-mx-4 sm:-mx-12 lg:-mx-16</code></td><td>负边距让区块<strong>反向吃掉父级 padding</strong>,出血到内容边</td></tr><tr><td><code>max-w-(--ui-container)</code></td><td>但又被 Nuxt UI 容器宽度锁住,不会越界</td></tr></tbody></table><p>参考本项目 <code>app/components/landing/Testimonials.vue</code> 的 <code>UCarousel</code> viewport 配置。</p><hr/><h2 id="before--after-">五、<code>before:</code> / <code>after:</code> 伪元素装饰</h2><h3 id="-hover---">父 hover 子才显 + 滑一下(三件套)</h3><pre class="language-html lang-html"><code class="language-html lang-html">&lt;div class=&quot;group&quot;&gt;
  &lt;UIcon class=&quot;
    opacity-0
    group-hover:opacity-100
    group-hover:translate-x-1
    transition-all
  &quot; /&gt;
&lt;/div&gt;</code></pre><ul><li>父级挂 <code>group</code></li><li>子元素常驻隐藏(<code>opacity-0</code>)</li><li><code>group-hover:*</code>:父级 hover 时子才响应</li><li>配 <code>transition-all</code> 做丝滑过渡</li></ul><p>参考本项目 <code>app/components/landing/Blog.vue</code> 的 &quot;Read Article&quot; 箭头按钮。</p><blockquote><p>适用:hover 时才渐显的图标、徽章、箭头、装饰元素。</p></blockquote>
<h3 id="">大引号装饰</h3><pre class="language-html lang-html"><code class="language-html lang-html">&lt;p class=&quot;
  before:content-[open-quote]
  before:text-7xl
  before:text-dimmed
  before:absolute
  before:-ml-10
  before:-mt-4
  after:content-[close-quote]
  after:...对称
&quot;&gt;
  推荐语正文...
&lt;/p&gt;</code></pre><p>关键点:<code>content-[open-quote]</code> 是 Tailwind 4 任意值语法,编译成 <code>content: open-quote</code>,引号自动按文档 <code>lang</code> 渲染(英文 <code>&quot;</code> / 中文 <code>「」</code>)。</p><p>参考本项目 <code>app/components/landing/Testimonials.vue</code> 的 <code>UPageCTA</code>。</p><hr/><h2 id="nuxt-ui-semantic-colors">六、Nuxt UI 主题色别名(Semantic Colors)</h2><p>这些 class 在 Tailwind 官网搜不到,但<strong>项目里到处都是</strong>。<a href="https://ui.nuxt.com/docs/theme">查看 Nuxt UI 主题文档</a>。</p><h3 id="">颜色别名</h3><table><thead><tr><th>别名</th><th>含义</th></tr></thead><tbody><tr><td><code>bg-default</code></td><td>默认页面背景</td></tr><tr><td><code>bg-muted</code></td><td>次要表面色(卡片淡底)</td></tr><tr><td><strong><code>bg-elevated</code></strong></td><td>凸起表面(卡片 / 浮层 / 选中态),比 <code>muted</code> 稍亮</td></tr><tr><td><code>bg-accented</code></td><td>进一步加 accent</td></tr><tr><td><code>bg-highlighted</code></td><td>高亮选中</td></tr><tr><td><code>bg-inverted</code></td><td>反色背景</td></tr><tr><td><code>text-default</code></td><td>主文本色</td></tr><tr><td><strong><code>text-muted</code></strong></td><td>次要文字</td></tr><tr><td><strong><code>text-dimmed</code></strong></td><td>极次文字(装饰元素、图标描边常驻)</td></tr><tr><td><code>text-hi</code></td><td>强调文字</td></tr><tr><td><code>text-inverted</code></td><td>反色文字</td></tr></tbody></table><h3 id="">高频组合</h3><h4 id="">半透卡片底</h4><pre class="language-html lang-html"><code class="language-html lang-html">&lt;div class=&quot;bg-elevated/60&quot;&gt;...&lt;/div&gt;</code></pre><p>60% 透明的凸起色,做卡片「轻微浮起」感。</p><h4 id="hover-">hover 轻微反馈</h4><pre class="language-html lang-html"><code class="language-html lang-html">&lt;div class=&quot;hover:bg-muted/50&quot;&gt;...&lt;/div&gt;</code></pre><p>鼠标进入变次要色 50% 透明,轻微 hover 反馈,不抢眼。</p><h4 id="nuxt-ui-">激活态文字色切换(Nuxt UI 标配!)</h4><pre class="language-html lang-html"><code class="language-html lang-html">&lt;button class=&quot;
  data-[state=active]:text-highlighted
  data-[state=inactive]:text-muted
&quot;&gt;
  ...
&lt;/button&gt;</code></pre><p>Reka UI 状态切换时自动 toggle <code>data-state</code>,纯 CSS 被动响应。</p><p>参考本项目 <code>app/components/landing/FAQ.vue</code> 的 <code>UTabs</code> 样式覆盖。</p><hr/><h2 id="group--group-hover-">七、<code>group</code> + <code>group-hover:</code> 联动机制</h2><p><strong>父级挂 <code>group</code>,子用 <code>group-hover:*</code> 响应父级 hover</strong>——Tailwind 不写 JS 切 class 的核心模式。</p><pre class="language-html lang-html"><code class="language-html lang-html">&lt;div class=&quot;group&quot;&gt;
  &lt;!-- 子元素常驻不可见,鼠标 hover 到父级任意位置时才显 --&gt;
  &lt;Icon class=&quot;opacity-0 group-hover:opacity-100 group-hover:translate-x-1 transition-all&quot; /&gt;
&lt;/div&gt;</code></pre><p>要点:</p><ul><li>子元素自己常驻隐藏(<code>opacity-0</code>),鼠标根本 hover 不到它</li><li>鼠标 hover 父级任意位置 → 子响应</li><li>配合 <code>group-data-[state=open]:*</code> 还能响应父级特定 data-state</li></ul><blockquote><p>网页动效 80% 的 hover 联动都靠它,React / Vue 项目通用。</p></blockquote>
<hr/><h2 id="">八、动画性能套件</h2><p>折叠 / 抽屉 / Tab 切换等动画场景,给做 transform 的元素加这两个,防掉帧:</p><pre class="language-html lang-html"><code class="language-html lang-html">&lt;div class=&quot;transform-gpu will-change-transform&quot;&gt;
  ...
&lt;/div&gt;</code></pre><table><thead><tr><th>Class</th><th>含义</th></tr></thead><tbody><tr><td><code>transform-gpu</code></td><td>强制该元素用 GPU 合成层渲染</td></tr><tr><td><code>will-change-transform</code></td><td>CSS hint,提前告诉浏览器「这元素的 transform 即将变化」,提前优化</td></tr></tbody></table><p>参考本项目 <code>app/components/landing/FAQ.vue</code> 的 <code>UAccordion</code> trigger 样式。</p><hr/><h2 id="">九、其他常用速记</h2><table><thead><tr><th>场景</th><th>Class 串</th></tr></thead><tbody><tr><td>长文字省略号</td><td><code>truncate</code> (= <code>overflow:hidden; text-overflow:ellipsis; white-space:nowrap</code>)</td></tr><tr><td>短文本行宽平衡</td><td><code>text-balance</code>(浏览器自动平衡每行长度,推荐语尾部不会只剩 1-2 字)</td></tr><tr><td>防止意外换行</td><td><code>text-nowrap</code></td></tr><tr><td>紧凑卡片悬停反馈</td><td><code>hover:bg-muted/50</code> + <code>rounded-lg</code> + <code>px-3 py-2</code></td></tr></tbody></table><hr/><h2 id="-cheatsheet">完整 Cheatsheet</h2><table><thead><tr><th>场景</th><th>关键 class 串</th></tr></thead><tbody><tr><td>顶部悬浮居中浮岛</td><td><code>fixed top-X left-1/2 -translate-x-1/2 z-10</code></td></tr><tr><td>毛玻璃胶囊</td><td><code>bg-muted/80 backdrop-blur-sm rounded-full border border-muted/50 shadow-lg shadow-neutral-950/5</code></td></tr><tr><td>父 hover 子才显 + 滑一下</td><td><code>opacity-0 group-hover:opacity-100 group-hover:translate-x-1 transition-all</code>(父挂 <code>group</code>)</td></tr><tr><td>压过第三方默认样式</td><td><code>pt-0!</code> / <code>px-0!</code>(Tailwind 4 important 后缀)</td></tr><tr><td>Reka UI 状态切文字色</td><td><code>data-[state=active]:text-highlighted data-[state=inactive]:text-muted</code></td></tr><tr><td>子元素响应父级 data-state</td><td><code>group-data-[state=open]:rotate-135</code>(父挂 <code>group</code>)</td></tr><tr><td>出血到全宽 + 锁 UI 容器宽</td><td><code>-mx-4 sm:-mx-12 lg:-mx-16 max-w-(--ui-container)</code></td></tr><tr><td>装饰性 CSS 大引号</td><td><code>before:content-[open-quote] before:text-7xl before:absolute before:-ml-10</code></td></tr><tr><td>动画不卡 GPU 提升</td><td><code>transform-gpu will-change-transform</code></td></tr><tr><td>简单大字 + 平衡行宽</td><td><code>text-balance</code></td></tr><tr><td>防换行</td><td><code>text-nowrap</code></td></tr><tr><td>长文字省略号</td><td><code>truncate</code></td></tr><tr><td>半透卡片底</td><td><code>bg-elevated/60</code></td></tr><tr><td>hover 轻微反馈</td><td><code>hover:bg-muted/50</code></td></tr></tbody></table><hr/><h2 id="">常见疑问</h2><p><strong>Q: 为什么 <code>bg-elevated</code> / <code>text-muted</code> 等在 Tailwind 官网搜不到?</strong></p><p>A: 这些是 <strong>Nuxt UI 自己注入的语义化颜色别名</strong>(Semantic Colors),基于 CSS 变量,自动跟随 light/dark 模式切换。要在 <a href="https://ui.nuxt.com/docs/theme">Nuxt UI 主题文档</a> 的 Semantic Colors 章节查,不是 Tailwind 原生类。</p><p><strong>Q: <code>!</code> 后缀是什么?</strong></p><p>A: Tailwind 4 的 important 语法,放在 class 末尾(如 <code>pt-0!</code>),等价于 <code>padding-top: 0 !important</code>,主要用来压过第三方库的默认样式。</p><p><strong>Q: <code>group</code> 和 <code>group-hover:</code> 必须配套吗?</strong></p><p>A: 是的。<code>group</code> 标在父级,<code>group-hover:*</code> 才能在父级 hover 时生效。单独写 <code>group-hover:*</code> 而父级没 <code>group</code>,Tailwind 不会识别父级关系,会失效。</p><hr/><blockquote><p>后续遇到新的经典套路继续追加到本文档。</p></blockquote></div><p style="text-align:right"><a href="https://sin6626.me/posts/source-code-study/portfolio-template-ai#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://sin6626.me/posts/source-code-study/portfolio-template-ai</link><guid isPermaLink="true">https://sin6626.me/posts/source-code-study/portfolio-template-ai</guid><dc:creator><![CDATA[Sin]]></dc:creator><pubDate>Mon, 20 Jul 2026 13:56:07 GMT</pubDate></item><item><title><![CDATA[Zod]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://sin6626.me/posts/study&amp;think/zod">https://sin6626.me/posts/study&amp;think/zod</a></blockquote><div><blockquote><p>Zod的学习, 非技术分享</p></blockquote>
<h2 id="zodhttpsgithubcomcolinhackszod">什么是<a href="https://github.com/colinhacks/zod">Zod</a></h2><p>Zod 是一个 帮助Ts的使用者们在Ts的编写和运行时，进行类型的推断和校验的库，极大的维护了Ts类型安全，0外部依赖， 轻巧，十分流行。</p><h2 id="zod">Zod的简单用法</h2><p>下面是在NuxtContent里面看见的Zod的用法，今天看文档的时候看见了Zod，发现不会使用于是就学习了下</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// 代码
      schema: z.object({
        // Hero 区: CTA 按钮组 + 跑马灯图片组
        hero: z.object({
          links: z.array(createButtonSchema()),
          images: z.array(createImageSchema())
        }),</code></pre><p>Zod的用法很简单，简单到把上面的代码翻译一遍给你听你就知道他应该如何去使用了</p><ul><li>schema必须是一个对象类型，下面必须要有hero属性</li><li>hero属性必须是一个对象，必须要有links和images属性</li><li>links必须是一个数组，并且数组内的元素类型必须是creatButtonSchema() 这个方法的返回值类型，或者说通过这个返回值的校验（这个方法名的后缀是Schema其实从侧面说明了，这个方法的返回值也是一个z。object约束的对象）</li><li>images跟links同理
对吧，就是一看就会，一用也会（大概？）。然后你可以使用parse() 去验证你的数据，好比这样schema.parse(data)。如果在运行的时候parse的校验没有通过，那么会直接报错，而Ts只有当你使用这种错误数据类型的时候才会报错（比如你在String类型上使用了toFixed方法），这样也说明了他比Ts更加的安全，注重运行的数据安全。</li></ul><p>如果你不希望他在运行时报错，你可以使用safeParse(使用这个api往往表示你希望数据是正确的)，方法会返回一个对象：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// 代码
{
  success:false,
  error:ZodError
}</code></pre><p>如果数据正确则会返回这样的对象：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// 代码
{
  success:true,
  data:{
    name:&quot;Tom&quot;,
    age:18
  }
}</code></pre><p>在使用Ts的前端开发中，你想要去校验一个表单数据，不需要先interface 再去写校验数据逻辑了， 直接可以使用Zod去规定一个schema， 然后使用parse去验证，或者使用type register = z.infer<typeof></typeof>直接生成一个interface，而且还有校验的规则。</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// 代码
const RegisterSchema=z.object({

 username:z.string(),

 age:z.number()

})


type RegisterForm = z.infer&lt;typeof RegisterSchema&gt;</code></pre><h2 id="">总结</h2><p>Zod的出现给运行时候的Ts代码套上一层安全的外壳，也简便了一些基础的校验规则，更多特性等有需求的时候再看吧。</p></div><p style="text-align:right"><a href="https://sin6626.me/posts/study&amp;think/zod#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://sin6626.me/posts/study&amp;think/zod</link><guid isPermaLink="true">https://sin6626.me/posts/study&amp;think/zod</guid><dc:creator><![CDATA[Sin]]></dc:creator><pubDate>Wed, 15 Jul 2026 14:28:03 GMT</pubDate></item></channel></rss>