关于 no-ui-flash
面向客户端渲染应用开发者的设计技能,指导在数据和状态确认前选择不会误导用户的占位界面,并涵盖边缘检查、提示 Cookie 与时序测试。
主要功能
- 01在边缘处理身份验证和偏好
- 02用提示 Cookie 提前呈现状态
- 03为状态变化设计安全回退
- 04验证返回路径并保留深层链接
- 05延迟探测以测试加载界面
适用场景
- 01避免访客短暂看到受限页面外壳
- 02减少搜索无结果时的骨架屏误导
- 03防止主题偏好加载后界面闪变
- 04测试登录跳转期间的深层链接
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rhyssullivan/ skills --skill no-ui-flash安装后在 Claude Code 中输入/no-ui-flash确认已加载。
详细介绍
永远不要闪现错误的 UI
客户端渲染的应用在首次绘制与客户端数据加载完成之间会有一个间隙。经典问题是用一个押注某种结果的占位内容填满这个间隙——结果押错了:
- 已退出登录的访客即将被重定向到登录页,却先看到应用外壳骨架屏(侧边栏、导航栏、内容卡片)。他们看到的是永远无法进入的应用,随后界面骤然切换。
- 搜索/列表页面上的结果网格骨架屏最终显示“未找到结果”——骨架屏承诺了实际不存在的内容。
- 首次绘制时使用浅色主题,偏好加载后却切换为深色主题。
- 某个最终显示为 404 的路由却先显示骨架屏。
规则:为已确认的状态渲染占位内容,而不是为期望中的状态渲染。 如果无法确认,就渲染在所有结果下都正确的占位内容(中性、布局稳定),而不是理想情况下的占位内容。要做到这一点,需要三层处理;每一层都会减轻下一层的工作。
第 1 层 —— 在边缘层解析状态,再提供文档
提供 SPA HTML 的服务器/边缘层通常已经掌握了客户端需要往返一次才能发现的信息。请在边缘层使用这些信息:
- 身份验证:大多数会话方案无需上游往返即可验证——密封/签名 Cookie 使用本地加密算法验证,JWT 则使用缓存的 JWKS 验证。对文档请求进行拦截:已退出登录 → 在任何应用 HTML 生成之前,执行
302 /login?returnTo=<path>重定向。若错误的应用外壳从未被提供,就不可能闪现。现在,任何通过受限路径收到 SPA 的人都已确认处于登录状态——客户端的骨架屏也因此不会造成误导。 - 由数据决定的状态:如果边缘层可以低成本判断“为空还是已填充”(例如计数值、KV 标记、记录上次已知状态的 Cookie),就可以提供正确的变体——空状态 HTML、已填充的应用外壳、重定向到引导页——而不是所有情况都使用同一份骨架屏文档。
- 偏好设置(主题、语言区域、密度):在边缘层读取偏好 Cookie,并在初始 HTML 中提供正确的变体。在
<html>上添加类名,胜过客户端切换。
容易踩坑的边缘情况(以身份验证为例,但同样适用于任何由 Cookie 携带的状态):
- 只拦截文档导航(携带
sec-fetch-dest: document的 GET/HEAD 请求;若没有该标头,则回退检查Accept: text/html)。API 路由、模块请求和健康探测请求应自行处理。 - 清除无效的状态载体,而不只是绕过它们。 无法通过验证的 Cookie 比没有 Cookie 更糟——任何依赖它存在的逻辑都会持续出错,直到它被清除。请在响应中让它过期。
- 持久化检查过程中轮换的任何内容。 如果验证刷新了令牌,新值必须通过此响应送达浏览器——刷新令牌通常只能使用一次,丢弃轮换后的令牌会悄无声息地导致会话之后失效。这适用于提供页面的路径,不只是重定向。
- 边缘检查中发生的故障应回退到安全状态(已退出登录、默认主题),绝不能返回 500。
第 2 层 — 用于即时正确渲染的提示 Cookie
无论客户端通常通过什么方式在首次探测时获取信息(/me、首次搜索、获取偏好设置),都应将其快照写入非 HttpOnly Cookie,这样下次加载时无需等待就能正确渲染:
- 每当服务端确认状态时,客户端就写入该 Cookie,并在下次加载时读取它,在探测进行期间为乐观版本提供初始值。(身份验证:身份显示数据。搜索/列表: “此账号有数据”或上次的结果数量。主题:解析后的偏好设置。)
- 它只是提示,绝非权威信息。真正的授权和数据仍来自服务端;过期或伪造的提示最多只能改变短暂显示的占位内容。仅保留用户已知的显示数据;读取时验证架构,并将任何格式错误的内容视为不存在。
- 已解析的探测结果始终优先——一旦到达,立即协调状态。
- 底层状态失效时,必须在所有位置清除此 Cookie(注销、账号清除、偏好设置重置)——在这些响应中由服务端清除,并在探测结果与提示矛盾时由客户端清除。提示若在失效后仍存在,就会让错误的 UI 显得很可信。
- 在 SSR/水合应用中,应在挂载之后(通过 effect/state)读取 Cookie,而不是在首次渲染时读取——客户端首次渲染必须与服务端 HTML 匹配。切换只需一帧,不必等待一次往返。
采用第 1 层和第 2 层后,客户端的“加载中”占位内容只会出现在其确实正确的状态下(例如,已验证用户首次使用新浏览器访问),因此乐观骨架屏终于名副其实。
第 3 层 — 客户端回退机制成为安全网
应用内状态门控不再处理全新加载,而是处理会话中途发生的变化(在另一个标签页中注销、会话过期、数据在其他位置被删除):
- 如果状态变化会使当前 UI 失效,就通过完整导航进行切换,并在导航期间渲染中性的内容(空白的主题化屏幕)——绝不能显示刚刚失去的 UI 的占位内容。
- 未知路由需要真正的 404 页面,而不是应用外壳或骨架屏。如果没有
notFound路由,回退内容很可能就是那个意外地为无效 URL 渲染骨架屏的页面——请检查。
跳转后返回(returnTo),适用于会重定向的门控
如果边缘层执行重定向(登录、引导流程),深层链接必须在跳转后继续有效。尽量不要为此再添加一个 Cookie——如果流程采用 OAuth 形式,state 参数本来就会通过提供方原样往返,而且已由 CSRF 检查进行身份验证(在 Cookie 中固定 state,并在回调时进行时序安全比较)。直接使用它:state = base64url(JSON { nonce, returnTo })。一个值,一个 Cookie;攻击者无法在不破坏比较结果的情况下替换目标地址。
无论 returnTo 从何处传入(门控查询参数、登录页面、回调中解码后的 state),都要将其验证为同源相对路径:以 / 开头,但不能以 // 开头(协议相对路径伪装成了绝对 URL),也不能是 API 路径。其他任何情况都回退到 /。解码必须对所有输入都能正常处理——提供方可能会发送从未由你生成的 state 回调;无效内容应视为“没有 returnTo”,绝不能抛出异常。
测试加载窗口
这个 Bug 出现在一个时间窗口中,因此测试必须让该窗口保持打开:
- 拦截探测请求并延迟响应(例如,使用 Playwright
page.route拦截/me或搜索端点,并加入延迟)。断言延迟期间页面实际渲染的内容:对于被重定向的访客,目标页面上没有任何骨架屏元素;对于带有提示的用户,显示真实 UI,且需要一个标志证明页面渲染时探测请求尚未完成。 - 通过网络驱动完整的重定向往返:受限的深层链接 → 携带 returnTo 的重定向 → 提供方 → 回调 → 最终到达深层链接。然后测试伪造版本:非同源的 returnTo 完成流程后,最终到达
/。 - 将 Cookie 清理情况断言为 Set-Cookie 标头:无效的状态载体 → 清除(Max-Age=0),同时清除其提示;发生终止事件(注销等)时 → 两者都移除。
- 无需等待过期即可驱动轮换路径:如果载体是密封 Cookie,就使用服务端所用的同一个库将其解封,破坏内部令牌的签名,再重新密封。边缘层会将其识别为无效但可刷新:断言页面已送达、响应中已设置轮换后的值,并且重放时已使用过的值会被拒绝。