ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Astryx 导航目的地契约(navigation-destinations)深度解析:跨组件统一的导航安全规则与共享 sink 架构

Astryx 导航目的地契约(navigation-destinations)深度解析:跨组件统一的导航安全规则与共享 sink 架构 Astryx 导航目的地契约navigation-destinations深度解析跨组件统一的导航安全规则与共享 sink 架构【免费下载链接】astryxAn open source design system thats fully customizable and agent ready项目地址: https://gitcode.com/GitHub_Trending/as/astryx导读本文围绕 Astryx 设计系统仓库中的 family 契约文档 展开系统讲解「无论哪个组件绘制链接、无论导航是原生还是命令式的、无论是否接入框架路由器最终用户得到的导航行为都必须一致且安全」这一核心契约。你将掌握该 family 的成员判定规则、useLinkComponent与useClickableContainer两大共享 sink 的源码级工作原理、javascript:/vbscript:/data:text/html等危险 scheme 的规范化拦截细节以及当前main分支上尚未落地的采纳缺口对应 AST-005 规范 与 #5524 实现。一、背景为什么需要一个「导航目的地契约」Astryx 是一个完全可定制、面向 Agent 的设计系统。在真实应用中同一个导航目的地destination可能由多种组件、多种方式激活Link、BreadcrumbItem、Citation等组件直接渲染原生a通过LinkProvider或as属性把链接替换成 Next.js、React Router、TanStack Router 等框架的自定义链接组件ClickableCard、Token这类「放大点击面」的组件通过useClickableContainer把整块区域变成可点击表面并可能走window.open/window.location命令式导航。问题在于组件的外在形态、路由集成方式、点击面大小都不应该决定「一个被禁止的 destination 能否被执行」。这正是 navigation-destinations.md 的Intent所在——用户从任何 Astryx 组件激活链接时应得到相同等级的目的地安全保证组件组合不能创造出绕过共享规则的路径。这份契约由 AST-005 规范 作为决策依据支撑其中明确声明了两个核心决策DEC-1 — destination safety follows every navigation sink每个 Astryx 自有的、能激活调用方控制的目的地的路径都必须遵循同一条规范化 block 规则仅依赖 React DOM 是不够的因为 React 只保护它渲染出来的href不会检查传给自定义路由器的 props也不介入window.open/window.location。DEC-2 — embedded resources remain a separate policy导航目的地与内嵌资源图片、媒体、下载、CSS、fetch是两类不同的 sink不能用一个笼统的「安全 URL」布尔值混为一谈。二、成员规则谁属于这个 family2.1 判定标准一个组件属于该 family 的标准是可观察的职责当 Astryx 接受或派生一个「可以被组件渲染、委托或激活为导航」的目的地时该组件即加入。成员资格跟随职责本身而不是跟随组件的视觉分类或它当前使用哪个共享 hook。反过来以下情况不加入只是布局调用方自有链接的组件用 Astryx 自有的 ID 生成固定同文档链接、且不接受调用方控制的 destination 文本的组件调用方自有的 JSX、插件渲染器、回调——一旦控制权交给调用方就超出契约边界。2.2 当前成员清单文档明确列出的当前成员包括Avatar; BreadcrumbItem; Buttonlink 模式; Citation; ClickableCard; Item; Link; ListItem; Markdown链接; NavHeadingMenuItem; SideNavHeading 与 SideNavItem; Tab导航模式; Tokenlink 模式; TopNavHeading、TopNavItem、TopNavMenu、TopNavMegaMenuItem、TopNavMegaMenuFeaturedCard; TreeListItem这些成员大多能在packages/core/src下找到对应实现例如 SideNavHeading.tsx、TopNavHeading.tsx、NavHeadingMenuItem.tsx、TopNavMegaMenuItem.tsx、TreeListItem.tsx。2.3 协作方与排除项协作方collaboratorsuseLinkComponent、LinkProvider、useClickableContainer、React DOM 的原生 anchor 净化器、Markdown 的 parser/render 边界。排除项excludedOutline 用 Astryx 生成的#id链接AppShell 固定的 skip-to-content 片段作为 children 传入的任意链接Markdown 插件输出图片/媒体/资源 URL以及只组合成员、但不接受也不派生 destination 的组件。成员关系是开放式的任何满足上述规则的新 Core 组件即使它委托给现有成员或共享 hook也必须加入该 family对应规范中的 IR3——新导航表面必须先加入 family 并复用共享 owner 才能发布。三、共享 owner导航安全由谁负责契约文档明确了四个「共享 owner」Owner职责useLinkComponent负责把 destination 交给原生或自定义链接组件包括面向路由器的href与to两个接缝useClickableContainer负责放大表面上的命令式导航覆盖同标签页、新标签页、修饰键点击、中键点击等路径Markdown负责把不可信源文本解析成 destination并在渲染边界维持共享导航策略React DOM受支持的原生 anchor 净化器React 不介入的每一条路径都由 Astryx 自己负责下面结合源码逐一展开。3.1useLinkComponent链接组件解析与to注入源码位于 useLinkComponent.ts解析优先级为asprop LinkProvidercontext 原生a。export function useLinkComponent(as?: LinkComponentType): LinkComponentType { const ctx use(LinkContext); const resolved as ?? ctx?.component ?? a; return useMemo(() { if (resolved a) { return a; } return createLinkWithTo(resolved); }, [resolved]); }一个关键实现细节是createLinkWithTo当解析结果不是原生a即走自定义组件路径时会包一层透明 wrapper把to{href}与href一起传给自定义组件。这是为了让以to为导航 prop 的框架React Router、TanStack Router无需适配器即可直接工作原生a会无害地忽略未知的toprop。LinkProvider则在 LinkProvider.tsx 中通过 LinkContext.ts 把默认链接组件注入子树典型的用法是import Link from next/link; LinkProvider component{Link} App / /LinkProvideruseLinkComponent.test.tsx 对这套解析逻辑做了充分验证默认解析为原生a、as覆盖 provider、嵌套 provider 内层覆盖外层并断言自定义组件路径下to与href同为/test、而原生a不注入to。与契约的对应关系这一实现正是 FR2替代渲染不绕过规则和 FR6href与显式to都是独立 sink的落点。契约文档在「Adoption and exceptions」表中明确记录useLinkComponent的 custom provider 与as路径目前尚未接入共享规则main分支目前是原样转发href/to#5524 是被接受的实现方案。3.2useClickableContainer放大点击面的命令式导航源码位于 useClickableContainer.ts解决的是「嵌套交互元素」问题整卡可点击、但卡片内按钮/链接被点击时不应触发卡片动作。其核心流程onClick回调为disabled直接返回文本选区hasTextSelection不触发通过hasInteractiveAncestor检查点击目标是否落在嵌套交互元素INTERACTIVE_SELECTORS定义的一套完整选择器覆盖 button/a/input/各类 role 及[data-pressable-container]上是则退出触发调用方onClickProp若defaultPrevented则停止href存在时按激活方式分派target _blank或 Ctrl/Cmd 点击 →window.open(href, _blank, noopener)有interactiveRef→ 代理点击interactiveRef.current.click()让框架链接组件处理客户端导航否则 →window.location.href href。onMouseUp负责中键点击event.button 1且href存在、未命中嵌套交互元素时window.open(href, _blank, noopener)新标签页打开。与契约的对应关系FR1决策先于 sink、FR3替代激活方式不改变决策、FR5原生/自定义/命令式路径全覆盖中键与 Cmd/Ctrl 点击不是例外都落在这个 hook 上。契约文档同样记录了采纳缺口main分支目前把原始 destination 直接交给window.open/window.location尚未经过共享规则过滤。ClickableCard在 ClickableCard.tsx 中组合CarduseClickableContaineruseLinkComponent卡片表面本身没有 role/tabIndex可访问的交互语义由卡片内部隐藏的a/button提供屏幕阅读器播报真实交互元素视觉 hover/active 叠层覆盖整卡。Token的 link 模式Token.tsx则是「表面激活与隐藏链接一致」的代表成员——同时有href与onRemove时链接与删除按钮作为兄弟控件并由useClickableContainer接线。四、canonical concepts五个核心概念表契约文档用一张表定义了 family 的五组核心概念是理解后续不变量的基础ConceptValues or statesDefault semanticsStabilitydestination sourcecaller prop、parsed content、Astryx-derivedcaller/parsed 值需要检查Astryx 固定的片段天然安全currentsinknative anchor、custom router、imperative browser API每个 Astryx 自有的 sink 都执行同一导航决策currentdecisionaccepted 或 blockedaccepted 保持既有行为blocked 不导航currentactivationplain、new-tab、modified、middle-click、programmatic proxy激活方式不改变决策currentresource kindnavigation 或 embedded/fetched resource本 family 只负责导航current五、跨组件不变量FR1–FR8每条规则逐一解读契约文档定义了 8 条功能不变量下面结合源码与规范逐条展开FR1 — 每个调用方控制的导航目的地在到达 sink 之前必须先做决策。任何成员都不得在共享规则运行之前把 destination 交给自定义路由器或命令式浏览器 API。对应 AST-005 的 FR1。FR2 — 替代渲染不绕过规则。通过LinkProvider或as替换原生 anchor不能绕过 destination 处理。这正是useLinkComponent作为统一解析入口的意义——所有成员都经由它获取链接组件。FR3 — 替代激活方式不改变决策。_blank、Cmd/Ctrl 点击、中键点击、同标签页赋值、委托表面点击必须产生相同的 accept/block 决策。对应useClickableContainer中 plain/new-tab/middle-click 各分支共享同一目的地来源。FR4 — 被禁用的 scheme 不能执行。在完成浏览器兼容的 scheme 规范化后javascript:、vbscript:、data:text/html不得成为可执行导航。这条正是下一节要重点展开的规范化逻辑。FR5 — 被接受的 destination 保留浏览器行为。相对路径、片段、protocol-relative 目的地、普通 scheme继续支持原生与路由器导航、target和浏览器 affordance契约不重写、不解析、不过滤 host。FR6 — 自定义路由器的两个 props 都是 sink。提供的href与显式to要独立检查二者都不能通过 prop 优先级或 rest-prop 顺序绕过规则AST-005 的 FR7安全的显式to可保留其文档化优先级但不安全的显式to不得借 rest spread 蒙混过关。FR7 — disabled 与 rejected 是两回事。成员保留自己的 disabled 语义拒绝一个 destination 只是阻止导航不发明 disabled 状态、标签或视觉样式。FR8 — 资源处理不继承导航策略。图片、媒体、下载、CSS URL、fetch 目标走各自的 sink 专属契约同时处理导航与资源的成员只对本 family 的导航路径应用此规则。六、核心机制scheme 规范化与危险 scheme 拦截FR4 的落地细节最有技术含量。规范 FR2 要求scheme 检查必须移除 ASCII 控制字符U0000–U001F与U007F、去掉外层空白、并对 ASCII scheme 文本做大小写不敏感比较——这是为了防止用浏览器会忽略的字符把被禁 scheme 藏起来。Markdown 解析器在 parser.ts 中的isSafeUrl正是这一规则的现行实现function isSafeUrl(url: string): boolean { // 去除浏览器容忍、但能绕过朴素前缀检查的控制字符/空白 //例如 java\nscript:alert(1) const normalized url.replace(/[\x00-\x1f\x7f]/g, ).trim(); const lower normalized.toLowerCase(); if ( lower.startsWith(javascript:) || lower.startsWith(vbscript:) || lower.startsWith(data:text/html) ) { return false; } return true; }这段代码同时印证了规范化三要素控制字符剥离、trim、小写化。对应测试覆盖在 parser.test.ts 中非常完整普通链接click)被整体作为纯文本输出而不是生成 link 节点L59-65大小写混合JaVaScRiPt:alert(1)同样被拒L67-72vbscript:链接被拒L74-79data:text/html图片 src 被拒L81-91引用链接/引用图片定义中的危险 scheme 被拒L1161-1169用控制字符藏 schemejava\tscript:alert(1)也被拒——注释明确指出 angle-bracket destination 是唯一能含控制字符的定义形态引用图片与引用链接两个消费者都必须应用该检查L1171-1184。渲染边界在 Markdown.test.tsx 中验证了同一行为javascript:链接不会产生a、data:text/html图片不会产生img而https://example.com与相对路径/page正常渲染为链接L590-617。这里体现了 AST-005 的一个重要约束FR8/IR1parser 与 renderer 可以各自维护实现但一致性测试必须把两者的导航矩阵钉在同一结果上。文档在 Adoption 表中也注明 Markdown 的 parser 与 renderer 目前是分离实现一致性靠测试保持对齐。七、代表矩阵与允许的组件差异7.1 代表性矩阵Representative matrix契约文档给出了成员在共享不变量下的「成员 × 状态」对照这里保留关键几行Member and stateShared invariantDeliberate variationcomponent:Link/ 原生 anchorblocked destination 不执行React DOM 负责原生 sink 净化component:Link/ custom provider 或asblockedhref/to不进入 routerrouter 拥有被接受的客户端导航component:ClickableCard/ plain 或 modified click每条命令式/委托出口使用同一决策可见 Card 结构与嵌套交互处理保持局部化component:Token/ link with remove action表面激活与隐藏链接一致remove action 保持为独立兄弟控件component:Markdown/ parsed linkblocked source 不成为导航被拒源可渲染为文本资源策略保持独立navigation aggregate / member itemitem destination 使用共享 ownertree、tab、breadcrumb、side-nav、top-nav、menu 语义保持局部化component:Citation/ linked sourceblocked URL 不执行原生 anchor 路径与引用呈现保持局部化7.2 允许的组件差异AV1–AV5契约不要求所有成员长得一样它允许AV1 — 被拒呈现自由Markdown 可把被拒源渲染为文本自定义链接成员可渲染为无 destination props放大表面可直接省略命令式导航。AV2 — 原生 vs 路由导航自由成员可用 React DOM anchor、provider 级路由组件、按组件as覆盖、或命令式浏览器 API只要其公开契约需要该模式。AV3 — destination 词汇自由公开 API 可暴露href、含href的结构化条目数据、Citation 的url、或解析后的 Markdown 语法共享行为不要求改名已发布的 props。AV4 — target 与关系保留destination 被接受后成员的target、rel、referrer、download、外链呈现行为保持不变。AV5 — 内嵌资源独立Markdown 与 Citation 可各自拥有图片/资源策略不影响本 family 的导航矩阵。八、采纳状态与当前缺口Adoption and exceptions这是契约文档中「事实与愿景的分界线」必须如实看待Components or surfaceAdoptionCurrent deviation or limitationMarkdown parsed and rendered links已通过 parser/render 检查采纳共享契约parser 与 renderer 分离实现一致性靠测试对齐原生 React anchors含 Citationplatform owner依赖受支持的 React DOM 净化器而非 Core 谓词useLinkComponentcustom provider 与as路径pending shared owner当前main原样转发href/to#5524 为已接受实现useClickableContainer命令式路径pending shared owner当前main把原始 destination 交给window.open/window.location#5524 为已接受实现组合上述两个共享 hook 的组件inherited仅在相关共享 owner 缺口关闭后完成契约文档特别强调这些 pending 行是 FR1–FR6 的采纳缺口不是已批准的例外。当 #5524 以 exact-head 证据落地时本表必须更新为共享采纳并在verified_by中加入新增的聚焦测试。对照 AST-005 规范 的「Current-state impact」小节规范给出的实现路径非常明确为 custom-link 与命令式出口新增一个共享的 Core 导航谓词在自定义链接委托前应用于href与显式to在useClickableContainer的每次window.open与window.location赋值前应用保留现有的安全普通目的地行为与委托的interactiveRef.click()路径保持 Markdown 导航规则一致同时让内嵌资源策略保持独立在 exact-head 验证通过后把 family 的采纳状态从 partial 更新为 shared。规范同时划定了明确的non-goals不校验 URL 语法、不要求绝对 URL、不做 host 白名单不定义图片/媒体/下载/CSS/fetch 资源策略不净化调用方自有 JSX、自定义 Markdown 插件输出或调用方回调不替代 CSP、路由授权、服务端校验或应用级访问控制不新增允许绕过/配置规则的公开 prop不改变链接标签、target/rel、disabled 语义或普通 URL 解析。这些都是理解契约边界的重要约束。九、验证地图测试如何钉住每一条不变量契约文档的 Verification map 与规范 Verification 章节互相印证ContractVerificationRepresentative members and statesMutation or failure expectationFR1、FR2、FR6useLinkComponent.test.tsxLink、Button、导航成员provider 与ashref与显式to自定义链接收到被拒 destination或 rest-prop 顺序把它恢复FR1、FR3、FR4、FR5由 AST-005 要求的聚焦useClickableContainer测试ClickableCard 与 Tokenplain、target、modified、middle click发生被拒的命令式调用或普通目的地丢失某种激活方式FR4、FR5parser.test.ts 与 Markdown.test.tsx解析链接、引用链接、autolink、混合大小写、控制字符、相对与普通 schemeMarkdown 接受了被拒 scheme或拒绝了普通导航目的地FR1、IR3由 AST-005 要求的 source/member 审计每个调用方控制的 Core 导航 destination新 sink 或 destination 组件脱离了成员快照FR7、FR8成员聚焦行为与资源套件禁用链接模式Markdown 链接 vs 图片Citation 链接 vs 图片destination 拒绝改变 disabled 语义或导航策略静默变成资源策略注意验证方法中的「mutation/failure expectation」这些测试的设计目标是在守卫被删除时失败对应规范的 IR2——每个不同的 sink 都要有 mutation-sensitive 覆盖。规范还要求真实 Chromium 验证 blocked 的原生/命令式目的地不执行、普通相对/外部/修饰键/中键导航仍可用自定义路由器 props 用单元集成测试验证不依赖特定路由包。十、决策链接、开放问题与内容边界决策链接spec:AST-005/DEC-1destination safety 跟随每个导航 sink、spec:AST-005/DEC-2内嵌资源保持独立策略详见 AST-005 决策日志。开放问题当前无。内容边界本文件负责导航目的地成员关系、跨组件 accept/block 结果与共享 sink 的采纳状态它不复制组件 prop 表、路由器实现细节、组件局部的 disabled/渲染行为、内嵌资源策略、当前审计结果或系统规范论证。总结与工程启示Astryx 的 navigation-destinations family 契约回答了一个非常实际的问题设计系统组件越多、可定制性越强越需要一个「任何组件、任何激活方式都不例外」的导航安全底线。它的工程方法论值得借鉴以「可观察职责」而不是组件分类来划分子系统边界成员规则把关键能力收敛到少数共享 owneruseLinkComponent、useClickableContainer避免每个组件维护自己的 blocked-scheme 列表IR1用规范化把攻击面堵死控制字符剥离 trim 大小写不敏感FR2/FR4用 mutation-sensitive 测试把「守卫存在」这件事钉死IR2诚实记录采纳缺口把「当前行为」与「目标契约」分开管理直到 #5524 等实现落地。对于正在接入 Astryx 的开发者请记得通过 LinkProvider 或as接入你的框架路由组件在 #5524 落地前自定义链接与useClickableContainer命令式路径仍由应用自行保证 destination 安全而所有经由原生a渲染与 Markdown 解析的链接已经在当前main上受到契约与测试的双重保护。【免费下载链接】astryxAn open source design system thats fully customizable and agent ready项目地址: https://gitcode.com/GitHub_Trending/as/astryx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表