十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Epic Stack 路由级对话框移除决策:为什么弃用 Route-based Dialogs 并改用独立页面与面包屑导航

Epic Stack 路由级对话框移除决策:为什么弃用 Route-based Dialogs 并改用独立页面与面包屑导航 Epic Stack 路由级对话框移除决策为什么弃用 Route-based Dialogs 并改用独立页面与面包屑导航【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack导读本文围绕 Epic Stack 仓库中的一份架构决策记录docs/decisions/023-route-based-dialogs.md展开剖析该项目为何决定移除路由级对话框Route-based Dialogs即把模态框/弹窗当作独立路由来渲染这一模式并阐释其替代方案将原对话框内容改造为普通页面同时引入面包屑breadcrumbs帮助用户定位与回溯。读完本文你将理解模态框在服务端渲染SSR架构下的 UX 与性能权衡、Epic Stack 对破坏性操作确认的推荐做法useDoubleCheckHook以及当前仓库中面包屑导航的实际落地实现以设置页为例可直接借鉴到自己的全栈应用中。一、决策背景Context模态框为何常常是糟糕 UX 的拐杖决策文档开篇就给出了一个相当尖锐的观点Dialogs (also known as modals) are often a crutch for poor UX design.即对话框又名模态框往往是糟糕用户体验设计的拐杖。它经常被用来掩盖页面没有针对用户意图做充分设计的事实。Epic Stack 的维护者认为在动手画一个弹窗之前更应该先想清楚这个页面在用户意图的上下文里应该长什么样、信息流该怎么组织。但这并不意味着对话框一无是处。文档明确指出它仍有两个合理的适用场景破坏性操作前的确认步骤例如删除数据前的二次确认。此时对话框给了开发者机会向用户解释接下来会发生什么而不只是一个孤零零的你确定吗。对于这种场景Epic Stack 原本已经有useDoubleCheckHook 来处理按钮级的二次确认而对话框则可以在动作完成前提供更多解释性信息。核心矛盾对话框与 SSR / 动画的冲突文档点出了使用对话框作为路由时的三个问题问题说明无动画的对话框体验差没有过渡动画的模态框弹出/关闭都显得生硬UX 质量低服务端渲染动画有代价如果在服务端渲染动画代码用户必须等待动画代码加载完成才能看到真正想要的内容首屏体验被拖慢同一 URL 两种形态的割裂同一个 URL 在客户端导航时是弹窗、直接落地访问时却是独立页面用户感知不一致参考案例Unsplash 的做法与局限文档引用了 Unsplash 的经典方案点击图片时以对话框形式展示图片详情但刷新页面后看到的是该图片的独立页面。Unsplash 团队显然是权衡过利弊后才做这个选择的但这种同一地址、两种形态的做法并不是通常情况下的好体验——因为它会造成页面状态与 URL 不对应刷新后形态突变客户端导航与直接访问体验不一致分享/收藏链接后对方看到的内容与你的预期不同。文档明确否定了这种模式在 Epic Stack 中的适用性。二、决策内容Decision从 Epic Stack 移除路由级对话框在写下这份决策日期 2023-07-14状态 accepted之前Epic Stack 在两处使用了路由级对话框2FA双因素认证流程头像编辑avatar edit体验。当初选择用路由承载这两者的理由是便于把用户直接链接到这些页面便于在页面之间导航进出前进/后退、刷新、分享。但按上面的分析这两个场景绝对不是路由级对话框的好用法——它们既不该在客户端导航时渲染成弹窗、落地访问时又渲染成别的形态像 Unsplash 那样。因此决策内容非常明确Remove route-based dialogs from the Epic Stack.从 Epic Stack 中移除路由级对话框。也就是说原先是弹窗的内容一律改成独立的普通页面。三、决策后果Consequences更好的 UX 与面包屑的引入决策的直接收益是更好的用户体验A better UX原来以弹窗形式出现的内容现在统一变成页面URL 语义稳定服务端渲染无需为动画代码买单刷新、分享、深链接行为一致。但代价也很现实弹窗天然带有从哪里来、回哪里去的上下文暗示改成页面后用户容易迷失。因此决策文档提出必须配套引入**面包屑breadcrumbs**导航帮助用户明确自己当前在站点层级中的位置orient themselves找到返回来源页面的路径find a way back to where they came from。这一决策即落地的思想在仓库源码中有完整实现。四、源码印证一useDoubleCheck—— 弹窗之外的破坏性操作确认方案决策文档提到对于破坏性操作确认Epic Stack 已经拥有useDoubleCheckHook。它的实现位于 app/utils/misc.tsxexport function useDoubleCheck() { const [doubleCheck, setDoubleCheck] useState(false) function getButtonProps( props?: React.ButtonHTMLAttributesHTMLButtonElement, ) { const onBlur: React.ButtonHTMLAttributesHTMLButtonElement[onBlur] () setDoubleCheck(false) const onClick: React.ButtonHTMLAttributesHTMLButtonElement[onClick] doubleCheck ? undefined : (e) { e.preventDefault() setDoubleCheck(true) } const onKeyUp: React.ButtonHTMLAttributesHTMLButtonElement[onKeyUp] ( e, ) { if (e.key Escape) { setDoubleCheck(false) } } return { ...props, onBlur: callAll(onBlur, props?.onBlur), onClick: callAll(onClick, props?.onClick), onKeyUp: callAll(onKeyUp, props?.onKeyUp), } } return { doubleCheck, getButtonProps } }其交互契约是第一次点击进入待确认态第二次点击才真正执行第一次点击preventDefault()阻止默认行为doubleCheck置为true按钮文案变为Are you sure?第二次点击doubleCheck已为true不再拦截正常触发onClick失焦onBlur或按下 Escape重置为初始态避免误触连锁。该行为由 app/utils/misc.use-double-check.test.tsx 中的两个测试用例锁定prevents default on the first click, and does not on the second与blurring the button starts things over验证了首次点击阻止默认行为、二次点击放行以及失焦即重置。在移除路由级对话框之后useDoubleCheck承担了原本需要弹窗完成的破坏性操作确认职责在仓库中的实际使用者包括app/routes/settings/profile/photo.tsx删除头像app/routes/settings/profile/two-factor/disable.tsx禁用 2FAapp/routes/admin/cache/index.tsx清除缓存等。以禁用 2FA 为例app/routes/settings/profile/two-factor/disable.tsxexport default function TwoFactorDisableRoute() { const disable2FAFetcher useFetchertypeof action() const dc useDoubleCheck() return ( div classNamemx-auto max-w-sm disable2FAFetcher.Form methodPOST p Disabling two factor authentication is not recommended. However, if you would like to do so, click here: /p StatusButton variantdestructive status{disable2FAFetcher.state loading ? pending : idle} {...dc.getButtonProps({ className: mx-auto, name: intent, value: disable, type: submit, })} {dc.doubleCheck ? Are you sure? : Disable 2FA} /StatusButton /disable2FAFetcher.Form /div ) }可以看到文案随doubleCheck状态在Disable 2FA与Are you sure?之间切换配合StatusButton的pending状态给出提交反馈既保留了操作前解释确认的体验又避免了弹窗在 SSR 场景下的动画加载问题。五、源码印证二面包屑在设置页的实际落地决策文档提出的面包屑方案在个人设置settings/profile路由树下有完整的参考实现。1. 用路由handle声明面包屑片段每个参与面包屑层级的路由通过导出的handle对象声明自己的面包屑内容。先看父布局 app/routes/settings/profile/_layout.tsxexport const BreadcrumbHandle z.object({ breadcrumb: z.any() }) export type BreadcrumbHandle z.infertypeof BreadcrumbHandle export const handle: BreadcrumbHandle SEOHandle { breadcrumb: Icon namefile-textEdit Profile/Icon, getSitemapEntries: () null, }这里用 zod 定义了一个BreadcrumbHandle结构约定handle.breadcrumb字段并与 SEO 的SEOHandle组合。子路由同样遵循该约定例如app/routes/settings/profile/change-email.tsxbreadcrumb: Icon nameenvelope-closedChange Email/Iconapp/routes/settings/profile/connections.tsxbreadcrumb: Icon namelink-2Connections/Iconapp/routes/settings/profile/passkeys.tsxbreadcrumb: Icon namepasskeyPasskeys/Iconapp/routes/settings/profile/password.tsx 与 app/routes/settings/profile/password_.create.tsxbreadcrumb: Icon namedots-horizontalPassword/Iconapp/routes/settings/profile/photo.tsxbreadcrumb: Icon nameavatarPhoto/Icon2FA 子布局 app/routes/settings/profile/two-factor/_layout.tsxbreadcrumb: Icon namelock-closed2FA/Icon每个面包屑都配了一个语义化图标与文字保持视觉统一。2. 用useMatches聚合路由链渲染面包屑布局组件的默认导出app/routes/settings/profile/_layout.tsx通过useMatches()拿到当前 URL 匹配到的整条路由链再逐一校验handle.breadcrumb后渲染const BreadcrumbHandleMatch z.object({ handle: BreadcrumbHandle, }) export default function EditUserProfile() { const user useUser() const matches useMatches() const breadcrumbs matches .map((m) { const result BreadcrumbHandleMatch.safeParse(m) if (!result.success || !result.data.handle.breadcrumb) return null return ( Link key{m.id} to{m.pathname} classNameflex items-center {result.data.handle.breadcrumb} /Link ) }) .filter(Boolean) return ( div classNamem-auto mt-16 mb-24 max-w-3xl div classNamecontainer ul classNameflex gap-3 li Link classNametext-muted-foreground to{/users/${user.username}} Profile /Link /li {breadcrumbs.map((breadcrumb, i, arr) ( li key{i} className{cn(flex items-center gap-3, { text-muted-foreground: i arr.length - 1, })} Icon namearrow-right sizesm {breadcrumb} /Icon /li ))} /ul /div Spacer sizexs / main classNamebg-muted mx-auto px-6 py-8 md:container md:rounded-3xl Outlet / /main /div ) }实现要点入口固定首个面包屑固定链接到用户主页/users/${user.username}文案为 Profile路由链自动聚合useMatches()返回从根路由到当前路由的匹配链凡导出handle.breadcrumb的路由都会按顺序成为面包屑的一环安全解析用BreadcrumbHandleMatch.safeParse(m)做运行时校验未声明面包屑的路由会被静默跳过返回null后filter(Boolean)过滤视觉层级非末级面包屑使用text-muted-foreground弱化级与级之间用arrow-right图标分隔末级即当前页突出显示SEO 配合这些设置子页面通过getSitemapEntries: () null排除在 sitemap 之外与SEOHandle组合避免设置页污染站点地图——相关约定可参见 docs/seo.md。3. 面包屑与可直达的页面协同当弹窗变页面之后每个设置项如 Photo、Change Email、2FA 等都有了稳定 URL配合面包屑用户与搜索引擎都能通过 URL 直接定位任意设置页满足当初方便链接用户直达的诉求通过面包屑判断当前在设置体系中的位置并一键跳回上一级替代弹窗关闭即返回的上下文。这正是决策文档所描述的更好的 UX把弹窗的隐性上下文转译成页面层级 面包屑的显性导航。六、这份决策对全栈应用的迁移启示结合上述源码证据可以把这份决策抽象成可复用的原则供其他 React Router / Remix 全栈应用参考默认页面而非路由弹窗除非有强理由如必须原地保持页面状态否则优先把功能做成独立页面URL 语义稳定、SSR 友好、便于深链与分享。动画不要阻塞首屏服务端渲染场景下别让动画代码成为用户看到内容前的加载门槛需要动画时考虑客户端渐进增强而非路由级弹窗。破坏性确认交给useDoubleCheck按钮级的两次点击确认 Escape/失焦重置足以覆盖大部分确认场景无需引入弹窗需要更多解释时直接在页面内用文字说明如 disable.tsx 中先解释禁用 2FA 不推荐再给按钮。面包屑是页面化的配套一旦移除弹窗务必用面包屑补偿从哪来、回哪去的上下文推荐用handle.breadcrumbuseMatches()的声明式方案让每个路由自描述布局统一聚合渲染。决策记录是项目的活文档本决策收录于 docs/decisions/README.md 所述决策记录体系——该目录收录我们为这个 starter 模板做出的所有决策并作为日后追溯为什么做这些决定的记录且决策永不是最终的鼓励后续推翻与修订。结语Epic Stack 对路由级对话框的取舍本质上是用稳定的 URL 语义和可预测的 SSR 体验换取弹窗式微交互的便利。这份决策文档篇幅不长却浓缩了三个可迁移到任何全栈项目的原则破坏性操作确认不必依赖弹窗、动画不应阻塞首屏内容、移除弹窗后必须用面包屑补齐导航上下文。仓库中useDoubleCheck的测试用例与settings/profile布局的面包屑实现为这套原则提供了完整可复用的代码范式。【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表