<?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>Fri, 16 Oct 2026 18:37:26 GMT</lastBuildDate><atom:link href="https://sin6626.me/feed" rel="self" type="application/rss+xml"/><pubDate>Fri, 16 Oct 2026 18:37:26 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[以 STEP 为框架理解 Plate 的测试实践：从全面检查到持续回归保护]]></title><description><![CDATA[<link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/1zer1pwvddwu7mamav.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://sin6626.me/posts/study&amp;think/step-framework-plate-testing-practices-regression-protection">https://sin6626.me/posts/study&amp;think/step-framework-plate-testing-practices-regression-protection</a></blockquote><div><blockquote><p>我们软件测试课程有课程分组任务，我选择了STEP模型，刚好最近Plate在重构中也在经历测试阶段了，借此机会我希望用STEP为框架去理解Plate的测试实践。本文只是自己的思考，肯定不100%正确，但是足够了</p></blockquote>
<h2 id="step">何为STEP模型</h2><p>STEP模型是软件测试中的一种测试模型，具体我感觉我也说不清楚，可能问Ai，才能知道最标准化的答案，我只能去回答一些自己对于这个事物的认知。</p><p>STEP模型思想，把测试融入进来的了开发的过程中，让开发和测试紧密结合（？？TDD测试驱动开发？？）。分为三个阶段</p><pre class=""><code class="">                        1. *Planning--测试规划*
                        2. *Acquisition--测试资源准备*
                        3. *Measurement--测试执行与评估*</code></pre><p>可能做个人项目的时候，不会考虑很多，就是一直写，考虑功能实现即可，也不会考虑什么地方可能会出现什么Bug，当然可能会自觉维护单元测试。然后，等东西做出来，自己使用的时候就发现了有很多的bug，然后就去修复，修复后自己又使用，发现bug确实修好了，那就心安理得的万事大吉了。后面又出现了Bug，代码改动太大了，不小心让之前的修好的bug回归了，然后又只有测试了单个的刚刚修好的bug，就导致bug回归了，你还被蒙在鼓里。然后你在修好一个bug的之后，你还会对之前的修好的bug进行测试，但是随着你bug越修越多，你每次修复的之后，测试的成本越来越大（因为之前修复好了很多的bug，每次修完bug还要测试之前修复的bug，防止bug回归）。为了解决这个问题，你又想到，在修复完bug后，我要留下一份自动化测试，以后让自动化测试测试bug有没有回归。这样一套，自上而下的思考，就可以引出来了STEP模型的思想。</p><p>现在自下而上的回归，我们开发的时候，就要去思考我们应该怎么测试，留下什么测试。写代码的时候，准备测试用例，测试数据，留下自动化测试，写完功能跑一遍自动化测试，但是还会有没有考虑到到的bug。如果后面实际使用中又出现了bug，那么我们就只需要在此基础上修bug和测试功能了，不用为之前的bug而烧心了，积累的自动化测试会保护好已覆盖的行为，持续回归保护。</p><h2 id="stepplate">STEP思想去理解Plate测试</h2><p><img src="https://s3.sin6626.me/mx-images/1zer1pwvddwu7mamav.png" alt="image.png"/></p><p>我们把Plate目前要做的测试按照STEP的三个阶段去理解：</p><table><thead><tr><th>STEP阶段</th><th>核心问题</th><th>Plate中具体的工作</th></tr></thead><tbody><tr><td>Planning</td><td>明确测什么，怎么测试，谁来测</td><td>1.1～1.5 ，1.7Natamox 去规划，整理好分工文档，提出Pr.</td></tr><tr><td>Acquition</td><td>开始测试资源准备什么</td><td>1.6，1.5 Ziad植入已知的Bug，把测试环境准备好，安装好.Natamox准备好页面检查脚本，测试方法</td></tr><tr><td>Measurement</td><td>产品标签如何，测试方法是否够好</td><td>1.8~1.10 开始测试，发布issue，评估测试方法</td></tr></tbody></table><p>这样梳理下感觉就清晰很多了，也方便我自己去理解了接下来要做的事情（等Ziad起床后安排好工作，然后就去实际测试录屏记录Bug，提交issue）。</p><p>其中这种提前植入Bug的方式我觉得还挺有意思的，不只是为了让我们找到它，还能帮助团队判断测试方法有没有发现问题的能力。检查“这个功能表现对不对”，是在评价产品。比较不同探索方法能发现多少已知问题，则是在评价测试过程。</p><p>比较有意思的是，测试计划还会记录每条回归测试源码的 hash。Agent 准备下一轮测试时，会读取上一轮的记录，再通过 <code>pnpm kb</code> 获取当前测试的 hash 和 CI 覆盖情况。如果 hash 变了，就说明这条测试被修改过，需要重新确认它还能检测出原来的 Bug。确认方式是让修改后的测试在修复前的代码上因原问题而失败，并在修复后通过。核验完成后，再更新记录中的 hash；否则，该问题仍保留在待复测清单中。</p><h2 id="tail">Tail</h2><p>STEP模型是一种测试的指导思想，但是我感觉，其实很多的时候不用去过分的去使用这种思想，很多事情其实都是水到渠成的，是经验主义？做多了就知道了（或许这些理论是从实践中总结出来的）。我感觉其实更要关注的是如何让Agent在写代码的做好测试，做一套很好的测试流程，让Agent的代码能更加的信任，lauren大佬就做好了这件事，视频中能学到很多。当前团队对于测试的看法是要以全自动测试作为长期的目标，不用人工测试。</p><p><a href="https://x.com/poteto/status/2102050467505430555">https://x.com/poteto/status/2102050467505430555</a></p><p>写文章我觉得是一件需要去坚持的下去的事情，尤其是当今信息流这么的庞大，安静的写写文章才能更好的引发起自己的思考。每次Astra用麻木的时候写写文章可以给自己醒醒脑，就算写写日常也好。</p></div><p style="text-align:right"><a href="https://sin6626.me/posts/study&amp;think/step-framework-plate-testing-practices-regression-protection#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://sin6626.me/posts/study&amp;think/step-framework-plate-testing-practices-regression-protection</link><guid isPermaLink="true">https://sin6626.me/posts/study&amp;think/step-framework-plate-testing-practices-regression-protection</guid><dc:creator><![CDATA[Sin]]></dc:creator><pubDate>Fri, 16 Oct 2026 12:03:18 GMT</pubDate></item><item><title><![CDATA[Plate V1 和 V2对比]]></title><description><![CDATA[<link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/07a4ycy30w5g0efkz5.png"/><link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/m4jv5f856ik590j0fi.png"/><link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/g4nbzcrkriro52gv4w.png"/><link rel="preload" as="image" href="https://s3.sin6626.me/mx-images/0o7mvbvmrtbic5tcie.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://sin6626.me/posts/study&amp;think/plate-v1-vs-v2-comparison">https://sin6626.me/posts/study&amp;think/plate-v1-vs-v2-comparison</a></blockquote><div><blockquote><p> 最近在Plate实习，老板让我先充分学习之后再去做功能，所以就有了这篇文章，搞清楚两个版本的对比，才能知道我现在正在做的事情到底是什么事情（个人理解，会有差错）</p></blockquote>
<h2 id="slate--plite">Slate 和 Plite</h2><blockquote><p>[!TIP]
V1 V2 不是指Plate包的版本，而是因为重构了底层编辑器引擎，所以我称之为V1 V2版本，但是我刚刚参与进来，很多底层我还不了解，内容请勿计较</p></blockquote>
<p>Slate是Plate v1 一开始使用的Rich-text editor 的框架和编辑引擎，但是Slate只做了很基础的底层的事情，并没有直接实现好富文本编辑器，富文本编辑器更多的功能，Plate选择使用一种插件体系去构成。需要什么功能就去使用什么插件。所谓的插件，可以理解为后端中很解耦的Modal层，需要什么功能就注册什么功能那种感觉，所以上我们的编辑器可以做到非常的轻，要什么就自己加什么插件就行，嫌麻烦可以一键使用Plate的基本工具包，我们称其为Kit。</p><p>v1的由于是基于Slate的，但是slate核心包里面是没有常用的Block Selection，只有最基础的Text Selection，想要在Plate使用Block Selection就需要加插件。为什么使用Slate好好的，要换成自研的Plite呢？老实说，我也不知道，我猜测方向可能是跟性能（Plite编辑引擎性能不应该比Slate差），便捷性（Plite把编辑器的一次修改组织成一个事务，里面可能包括多次的操作，类似数据库那样，提交事务后修改完成，也能方便进行Undo操作），归一性（让零碎的API归一起来，所以这一个版本API会有很大的变更，还有统一model，DOM选区和浏览器的输入），有可能有些历史原因吧（某些Pr）。都是我我的猜测和思考，不过话说我为什么不直接去问老板（🥲）</p><p>v2，也就是现在我们正在开发的Plate的新版本，团队把底层核心由Slate换成了自研的Plite。Plite在M层几乎有跟Slate几乎一样的数据结构，一样的思路。都是的Slate节点树，使用Text 去表示文字节点，页面有tpye表示文本的类型，还有属性去控制它的样式，Element底下是children，有节点在树中的位置Path，Range表示Text选区（anchor，focus，offset）等。得益于Plite和Slate有非常多相同的地方并且有这么好的数据结构去描述，所以它的理解起来很容易。但是Plite还有一个很大的变化就是它把之前的Selection的部分之前移动编辑器引擎层了，也就是它的core包里面，把Text Selection和Block Selection 和在了一起，由底层统一去管理<code>Selection = TextSelection | NodeSelection | null</code>，这样我们无需插件便可享受到Block Selection这项当好样的功能（当前我写文章这个版本的Lexical编辑器还不支持Block Selection，新版支持），当然这样做的坏处就是上层的功能需要重新的去做适配，不过显然这个改动是长期正确的改动。另外改动就是上端的猜测部分括号内的改动。</p><h2 id="radix-ui--base-ui">Radix UI 和 Base UI</h2><p>去年的12月，shadcn/ui支持将Base UI作为底层的primitive方案；今年的12月，shadcn/ui 正式将Base UI设为默认的方案。2020年，Radix UI 发布，快速推广，成为了最广泛使用的Headless组件库之一，23年，shadcn/ui发布，一开始就默认基于Radix UI 构建。我在网上搜寻了Base UI出现的原因和shadcn/ui愿意去默认使用Base UI的原因，Radix UI 在被 WorkOS 收购后维护放缓，而由 MUI 团队（包含Radix UI 的创始人--Colm Tuite）全职开发的 Base UI 在 React 19 兼容性、包体积和复杂组件支持方面展现出更强的竞争力。我感觉就是Radix UI被收购后，更新太慢了，太不稳定了，对于React 19的关键性功能支持迟迟不落地，完全看不懂前景了（当前Blog的前端还是Innei当年基于Radix UI 自研的组件库，还是Next 14版本，感叹时间真的飞速）</p><p>话说回来，这跟我们Plate有什么关系呢？其实早在今年5月份，Plate的discussions里面就有人询问了有关BaseUI 的问题，后续在8月份的讨论中阐明了真正的落地的时间</p><p><a href="https://github.com/udecode/plate/discussions/4974">https://github.com/udecode/plate/discussions/4974</a></p><p>zbeyens回复”It will be supported. Note that our components depend on shadcn components, so you could replace those with the base ones.“ 后续还补充到 ”Probably for the end of the year.“。</p><p>那么显然，我们新的一个版本就是要增加对于Base UI的支持，这也就是Plate v2正在做的事情。让Astra读仓库之后可知</p><p><img src="https://s3.sin6626.me/mx-images/07a4ycy30w5g0efkz5.png" alt="image.png"/></p><p>所以大概就是这样的情况</p><h2 id="">新功能</h2><p>我目前感知，可能就多了分页视图的功能，Details，别的更多的是底层和还有性能上的优化吧。分页视图功能可以想象为类似于Word那样的，有一张纸，有页边距和页间距的纸，内容根据排版来跨页展示结果，但是这里肯定是可以配置，符合Plate一贯插件式可配置式的风格。</p><p>下面是Details，其实早在V1版本的时候就有一个Toggle了，不过Toggle大道至简，功能很简单，就是一个可折叠的组件，里面能放子段落去展示内容，不支持一个Toggle里面又嵌套一个Toggle，要是你非要这样做的话，只能使用table制表符模拟，本身的节点还是平行结构的，也不可改Summary的样式，达不到Notion那种水平。</p><p>这次把Toggle 重构成了Details 组件，也为他新增了许多的新功能，真正意义上支持多级嵌套Details，因为他底层的节点树的结构就是支持嵌套的，不过现在还不支持改Summary的type，不支持把Text直接改成任意的Heading。当前如果你去把原本是Summary的Text改成Heading的话，那么Summary会直接变成其中一个子段落，然后又由于Details的插件规则自动生成一个空白内容的Summary，就是让你的Details变成类似于下面这样。</p><p><img src="https://s3.sin6626.me/mx-images/m4jv5f856ik590j0fi.png" alt="image.png"/></p><p>现在这种情况应该是说，如果不支持改变Summary改成Heading的行为，应该是要禁用这个改成Heading的功能才对，或者可以通过配置关闭这个功能（暂时没有实现），或者说我们应该支持这个功能，至少Noton是支持，我认为我们应该支持，不过现在我还没跟老板去说，或许他早有打算也不一定。</p><p>还有一些听GPT说的旧功能的重新设计和重写，还有我一些我暂未感知到的功能</p><p><img src="https://s3.sin6626.me/mx-images/g4nbzcrkriro52gv4w.png" alt="image.png"/></p><p><img src="https://s3.sin6626.me/mx-images/0o7mvbvmrtbic5tcie.png" alt="image.png"/></p><p>老实说，跟rich-text editor 还有一定缘分的，上课的时候听过10多年前的老家伙说过，“要是觉得你很技术，你就去开发一个富文本”，Innei在大学的时候也独自摸索过rich-text editor，改过Lexical，看他的文章也学到了rich-text editor另外的价值，自己也在摸索如何在长列表中渲染出更好的文本格式，OpenAI前几日全面开放GPT6 Sol 的时候推出了他们的Intelligent UI 让Chat 模式下的回复更加的好。</p><p>希望能在Plate中学到更多的东西，我很荣幸能参与到Plate的开源建设中，作为我第一个参与的开源项目，我会拿出很多的时间和精力去对待它，不仅仅是因为是公司的项目，更多的还是我对开源的喜爱。</p></div><p style="text-align:right"><a href="https://sin6626.me/posts/study&amp;think/plate-v1-vs-v2-comparison#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://sin6626.me/posts/study&amp;think/plate-v1-vs-v2-comparison</link><guid isPermaLink="true">https://sin6626.me/posts/study&amp;think/plate-v1-vs-v2-comparison</guid><dc:creator><![CDATA[Sin]]></dc:creator><pubDate>Mon, 12 Oct 2026 13:38:21 GMT</pubDate></item><item><title><![CDATA[从展示页到网站入口：首屏页（Shouping）的架构演进与开门动画转场实践]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://sin6626.me/posts/technology/shouping-splash-page-architecture-evolution-fullscreen-transition">https://sin6626.me/posts/technology/shouping-splash-page-architecture-evolution-fullscreen-transition</a></blockquote><div><p>很早就想试试网格布局了，刚好看见了imsyy多年前的项目布局，很喜欢，拿来改成了更加符合现代的技术栈的页面重新审视入口：从独立展示页到门户
最初，<code>shouping</code> 只是一个挂在 <code>/shouping</code> 路由下的独立实验页面，网站原有的内容流主页依然在 <code>/</code>。它的诉求清晰而纯粹：<strong>访客进入网站时，首先感知站点的调性与主人的气质，随后再决定是探索主站，还是直达特定工具</strong>。
随着视觉打磨与交互逻辑的逐渐成熟，它的角色发生了质的转变——它不再只是一个“附属展示页”，而应当是整个站点的“正门”。我们逐步将完整的站内生态、动态数据看板与沉浸式转场融入其中，最终将它与原首页平滑对调，确立为网站的正式入口（页面灵感来源于<a href="https://github.com/imsyy">(imsyy)</a> 多年前的新手项目）。
布局骨架演进：1:1 对称
左右对称的视觉平衡
页面最外层采用沉浸式全屏纯暗色背景（<code>bg-[#282a30]【NuxtUI默认】</code>），主体内容通过居中容器（<code>max-w-7xl mx-auto</code>）水平垂直居中。
很多页面习惯采用 5:7 或 4:8 的不对称划分，但在以卡片为核心的个人主页中，左右采用 <strong>严格的 1:1（<code>grid-cols-1 lg:grid-cols-2</code>）对称平分</strong> 才是视觉均衡的最佳解法（原本是想5:7分的，但是效果不好）：
<strong>左侧（个人名片区）</strong>：包含了悬停微旋转的圆形头像、个性化大字标语、深色毛玻璃一言卡片（古典双引号包裹）、以及社交平台矩阵；
<strong>右侧（信息展板与导航区）</strong>：横向宽度与左侧卡片严格保持一致，达成几何上的严谨呼应。
右侧 6 栅格体系（<code>grid-cols-6</code>）
右侧需要同时容纳两个信息看板（诗词、时钟天气）与六个快捷导航。我们采用了一个极其工整的 <strong>6 栅格体系</strong>：
<strong>上半部分</strong>：2 个信息卡片各占 3 列（<code>sm:col-span-3</code>，$3 + 3 = 6$），等宽平分一行；
<strong>下半部分</strong>：快捷导航卡片各占 2 列（<code>sm:col-span-2</code>，$2 \times 3 = 6$），一行整齐排布 3 个，两行共 6 个。</p><p>拒绝臃肿依赖：挖掘 Nuxt UI Carousel 的手势滑动潜力
当快捷导航项逐渐丰富（分为站内功能集与站外社交/主页），卡片开始超出单屏舒适容量。直觉上很多开发者会立刻 <code>pnpm install swiper</code> 原项目之前就是使用的swiper，根据我的技术栈重新换了个技术，选择优先挖掘当前技术栈（Nuxt UI）的内置能力，如果使用Reka UI作为项目的UI库我可能会使用Swiper/Embla Carousel。
为什么选择 <code>&lt;UCarousel&gt;</code> 而非外部 Swiper？
<strong>零额外依赖与包体积优化</strong>：避免为了一组简单的导航引入几十 KB 的 Swiper 运行时与额外的 CSS；
<strong>底层基于一流手势引擎</strong>：Nuxt UI 的 <code>&lt;UCarousel&gt;</code> 底层直接基于著名的 **<code>embla-carousel-vue</code>**，具备开箱即用的触摸滑动（Touch Swipe）、鼠标按住拖拽（Mouse Drag）、阻尼物理惯性与键盘无障碍切换；
<strong>双向数据驱动指示器</strong>：我们重构了 <code>navPages</code> 分页结构，并将外部经典的胶囊指示条与轮播事件双向绑定——用户手指或鼠标拖拽滑屏时，指示条通过 <code>@select</code> 实时变宽高亮；点击指示条圆点时，调用 <code>carouselRef.value?.emblaApi?.scrollTo(index)</code> 平滑滚到对应页。
真实世界的动态看板：时钟、天气与水合时区</p><p><strong>时钟跳动与 SSR 水合防御</strong>：
诗词卡片提供点击循环切换，而时钟卡片则由 <code>dayjs</code> 格式化日期，并结合 <code>@number-flow/vue</code> 呈现机械质感的数字平滑翻滚。
<strong>关键边界防御</strong>：由于 Nitro 服务端渲染（SSR）通常处于 UTC 时间，而客户端浏览器处于本地时区，时间与星期极易引发水合不一致（Hydration Mismatch）报错。时钟区域严格使用 <code>&lt;ClientOnly&gt;</code> 包裹并提供优雅的骨架屏 fallback，将时间计算收敛在客户端（项目中之前踩过这个坑）。
<strong>天气接口与本地缓存解耦</strong>：
城市与气温不能靠浏览器预设。我们将其从 UI 中完全抽离至 <code>app/Api/weather.ts</code>；
页面挂载后请求用户地理定位，获得经纬度后向 Open-Meteo 查询实时气温与天气状况；
<strong>30分钟缓存机制</strong>：天气数据连同坐标与时间戳写入 <code>localStorage</code>，半小时内重进页面优先读取缓存，避免因频繁切换路由向天气服务器重复请求；用户拒绝定位或弱网时，优雅降级为状态提示（没用使用VueQuery是因为觉得在当前接口很少的情况下，完全没有必要引入新的依赖包去增加自己的心智负担和打包后的体积）。
<strong>多语言路径安全（<code>useLocalePath</code>）</strong>：
站内所有链接使用 <code>@nuxtjs/i18n</code> 的 <code>localePath</code> 统一包装，确保用户不论在中文还是 <code>/en/</code> 英文环境下点击跳转，多语言上下文都不会被粗暴冲刷。
<code>@click</code> 的边界
在功能趋于完善时，我们经历了两次具有典型工程教育意义的细节排错与探索：</p><p><code>@click.capture</code> 与 <code>@click</code> 的实战真相
在拦截 <code>&lt;NuxtLink&gt;</code> 跳转以注入自定义转场动画时，代码中使用了 <code>@click.capture</code>。
<strong>原理剖析</strong>：<code>.capture</code> 是 DOM 事件流的“捕获阶段”，意在事件刚进入顶层时先发制人，截胡 <code>&lt;NuxtLink&gt;</code> 的默认路由处理；
<strong>实测解惑</strong>：实测改成普通的 <code>@click</code> 表现完全一致。这是因为浏览器的“跳转默认行为”必须等待**整个事件流（捕获 $\to$ 目标 $\to$ 冒泡）完全结束**之后才会执行。只要在冒泡阶段调用了 <code>event.preventDefault()</code>，依然能成功截停跳转。在内部没有子元素滥用 <code>stopPropagation</code> 的纯净结构下，使用语义更自然的 <code>@click</code> 即可。
“开门”转场：DOM 快照、卸载感知与无闪帧同步
Visit 按钮、头像以及所有站内导航，绝不仅仅是一次简单的 <code>router.push</code>，而是：<strong>首屏沿中间分割线向两侧缓缓撕裂滑开，露出门后准备就绪的主站。</strong>
这套复杂的动画被统一收敛在 <code>useEnter.ts</code> 中，背后经历了三层核心攻坚：</p><ol start="1"><li>视口裁剪与 NumberFlow 快照修复
我们通过 <code>fixed inset-0 z-[9999]</code> 创建全屏遮罩，利用 <code>sourcePage.cloneNode(true)</code> 完整复制当前视口画面，放入两扇各占 <code>50%</code> 宽度的面板容器中，配合 <code>overflow: hidden</code> 精确裁切出左半边和右半边；
<strong>时钟消失踩坑</strong>：<code>@number-flow/vue</code> 底层是一个封装了 Web Component 原生自定义元素（<code>&lt;number-flow&gt;</code>）的高级组件，shadow DOM，使用cloneNode无法穿透clone，两者不在一个DOM树下，最后是维护了一个H5的自定义属性，让DOM 树在clone的时候读取这个属性渲染页面的（这里应该是最大的坑点了，毕竟是好几年的库了，有坑很正常）。</li><li>为什么不能直接在 <code>navigateTo</code> 后面执行动画？（<code>isConnected</code> 与 <code>MutationObserver</code>）
<code>await navigateTo(to)</code> 仅仅代表路由切换指令发出了，此时 Vue 还需要异步加载新页面的 Chunk 并执行 setup。
如果直接执行动画，在开发服务器上， 动画首次开始执行的一瞬间，看到的会是<strong>旧页面白白停滞了几十毫秒，或者露出一瞬间的白屏，而生产服务器上没有这个问题，但是这也说明了代码不健全的问题</strong>；
我们使用 <code>sourcePage.isConnected</code>（浏览器原生 DOM 存在属性）配合 <code>MutationObserver</code>，递归监听 <code>#__nuxt</code> 根节点（<code>{ childList: true, subtree: true }</code>）。
<strong>只有当检测到&amp;#32;<code>!sourcePage.isConnected</code>（旧页面彻底从 DOM 树中被拔除、新页面正式就位）的瞬间，才放Promise（<code>resolve()</code>）通过&amp;#9;</strong></li><li>消灭白屏闪烁：<code>requestAnimationFrame</code>
即使旧 DOM 拔除了，在浏览器的事件循环机制中，<strong>DOM 变化（JS执行）并不等于像素已经画到屏幕上</strong>（还需要计算样式、布局与绘制 Paint）。
我们在开门前加入了：
强制让出主线程，等待浏览器把新页面的第一帧稳稳绘制到显卡上，再将门板向左右两侧优雅拉开。至此，白屏、频闪被彻底消除，丝滑度拉满。同时配合 <code>prefers-reduced-motion: reduce</code>，在用户开启减弱动画时自动跳过开门直接秒切，兼顾无障碍体验（requeAnimation在梅花生长动画中也有到了，而动画减弱判断则是在GSAP官方那学到的）。
路由平移与体验闭环
在所有动效、容错与状态机都经过严苛检验后，我们完成了最终的“移花接木”：
<strong>根路由对调</strong>：原本的 <code>/shouping</code> 正式进驻网站根路径 <code>/</code>，原主站流式页面后移至 <code>/home</code>；
<strong>闭环互通</strong>：首屏的 Visit 按钮、头像与“主站首页”卡片，通过双开门动画平滑进入 <code>/home</code>；而进入主站后，左上角头像点击可重返 <code>/</code> 门面；
<strong>环境验证</strong>：Nuxt 预渲染、多语言路径自动转换、SSR 本地构建与全量 TypeScript 类型检查均以 <code>Exit status 0</code> 零警告无缝收官。
昨天晚上加今天一下午完成了这个shouping页并且迁移，今天JOJO飙马野郎更新第二集，看看能不能改个黄金分割的动画😶‍🌫️</li></ol></div><p style="text-align:right"><a href="https://sin6626.me/posts/technology/shouping-splash-page-architecture-evolution-fullscreen-transition#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://sin6626.me/posts/technology/shouping-splash-page-architecture-evolution-fullscreen-transition</link><guid isPermaLink="true">https://sin6626.me/posts/technology/shouping-splash-page-architecture-evolution-fullscreen-transition</guid><dc:creator><![CDATA[Sin]]></dc:creator><pubDate>Fri, 25 Sep 2026 13:13:55 GMT</pubDate></item><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></channel></rss>