深入解析 Gutenberg components 中的 ExternalLink 组件:外链的渲染、锚点拦截与样式机制
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
ExternalLink是 WordPress Gutenberg 仓库@wordpress/components包中用于指向外部资源的标准链接组件。它比原生<a target="_blank">多做了三件事:强制新标签页打开、自动追加"新标签页打开"的可访问性图标与 aria-label、以及对页内锚点链接(#xxx)做默认行为拦截。读完本文,你可以准确使用该组件的children/href等 props,理解其 forwardRef 实现、点击事件链与 SCSS 样式细节,并了解它在当前仓库中已被标记为"不推荐"、应改用@wordpress/ui的Link组件这一现状。
基本用法与 Props 说明
ExternalLink从@wordpress/components导出(见 packages/components/src/index.ts 中的export { default as ExternalLink } from './external-link';)。组件文档(packages/components/src/external-link/README.md)给出的标准用法如下:
import { ExternalLink } from '@wordpress/components'; const MyExternalLink = () => ( <ExternalLink href="https://wordpress.org">WordPress.org</ExternalLink> );组件接受的核心 props 定义在 types.ts 中:
| Prop | 类型 | 必填 | 说明 |
|---|---|---|---|
children | ReactNode | 是 | 链接内展示的内容(可以是文本、图标或任意 React 节点) |
href | string | 是 | 外部资源的 URL;若以#开头则被视为页内锚点(见下文拦截逻辑) |
文档同时说明:除上述 props 外,其余 props 会透传到内部渲染的<a>元素上。从类型定义看(index.tsx),组件的 props 类型是Omit<WordPressComponentProps< ExternalLinkProps, 'a', false >, 'target'>——即继承自 WordPressComponentProps 的 HTML 锚点全量属性,但显式排除了target。这意味着业务方无法覆盖target的取值,组件内部固定写入target="_blank",保证所有外链一律在新标签页打开,同时通过/* eslint-disable react/jsx-no-target-blank */豁免了 ESLint 对rel="noopener"的告警(见 index.tsx)。
源码级渲染结构:contents 与 icon 两个 span
组件通过forwardRef(来自@wordpress/element)包装,并将 ref 转发到根<a>元素上,因此外部可以直接拿到锚点 DOM(index.tsx)。其渲染产物是如下结构:
<a className="components-external-link {自定义className}" target="_blank" href={href} onClick={onClickHandler}> <span className="components-external-link__contents">{children}</span> <span className="components-external-link__icon wp-exclude-emoji" aria-label="(opens in a new tab)" /> </a>这里有几个值得注意的实现细节:
- 文本下划线只覆盖内容,不覆盖图标:
children被包进components-external-link__contents,下划线样式(见下文样式小节)只作用于该 span,箭头图标保持独立。 - 国际化 aria-label:图标 span 的
aria-label使用__()函数翻译为(opens in a new tab),因此屏幕阅读器的完整链接名是"链接文本 + (opens in a new tab)"。这一可访问性表现被测试用例直接锁定:测试中用screen.getByRole( 'link', { name: 'WordPress.org (opens in a new tab)' } )定位元素(test/index.jsdom.test.tsx)。 - RTL 感知箭头:图标内嵌字符通过
isRTL()判断方向——RTL 布局下渲染\u2196(↖),LTR 下渲染\u2197(↗)(index.tsx)。 wp-exclude-emoji类:源码注释明确写道,该类的作用是"防止箭头被 Twemoji 替换成图片"(index.tsx),即 WordPress 表情替换机制会跳过这个字符,保证箭头始终是纯文本字符。className合并:外部传入的className通过clsx与基础类components-external-link合并(index.tsx)。
内部锚点拦截:为什么点击#anchor不会"打开编辑器"
这是ExternalLink最具 Gutenberg 特色的行为。组件会检查href是否以#开头,若是则视为页内锚点,并在onClick中调用event.preventDefault():
// Anchor links are perceived as external links. // This constant helps check for on page anchor links, // to prevent them from being opened in the editor. const isInternalAnchor = !! href?.startsWith( '#' ); const onClickHandler = ( event ) => { if ( isInternalAnchor ) { event.preventDefault(); } if ( props.onClick ) { props.onClick( event ); } };(见 index.tsx)
从源码注释可以推断其动机:Gutenberg 编辑器中target="_blank"的链接点击会被编辑器拦截并打开"预览面板",而页内锚点本应只是滚动定位,不应该触发编辑器预览,因此必须阻止默认行为。注意props.onClick无论是否锚点都会照常回调,且回调位于preventDefault()之后,所以业务代码拿到的事件对象上defaultPrevented已为true。
这一行为由 4 个 jsdom 测试完整覆盖(test/index.jsdom.test.tsx):
| 测试场景 | 断言 |
|---|---|
普通外链 +onClick | 点击后onClickMock被调用 1 次 |
#test锚点、无onClickprop | 直接挂到 DOM 上的onclick收到defaultPrevented: true |
#test锚点 +onClick | onClick被调用 1 次且defaultPrevented: true |
非锚点外链、无onClickprop | defaultPrevented: false,不拦截 |
其中"无onClickprop"的两个用例采用了巧妙写法:直接向渲染出的<a>挂link.onclick = onClickMock,从而在不经过 React props 的情况下验证原生事件链上的defaultPrevented状态(test/index.jsdom.test.tsx)。
样式实现:WPDS 设计令牌与焦点环
视觉样式定义在 style.scss,全部基于 WordPress 设计系统(WPDS)CSS 变量:
.components-external-link { color: var(--wpds-color-foreground-interactive-brand); text-decoration: none; &:hover, &:active { color: var(--wpds-color-foreground-interactive-brand-active); } &:focus { // Override style from wp-admin common.css. box-shadow: none; border-radius: 0; &:not(:active) { @include mixins.outset-ring__focus(); } } }- 链接本体(
components-external-link)无下划线,颜色取品牌交互色--wpds-color-foreground-interactive-brand,visited 状态保持同色,hover/active 切换为--wpds-color-foreground-interactive-brand-active; - 下划线被移到内容 span 上:
text-decoration: underline、text-underline-offset: 0.2em、text-decoration-thickness: from-font(style.scss); - 焦点态用
box-shadow: none+border-radius: 0显式覆盖 wp-admincommon.css的默认外发光,再在非 active 的 focus 上套用mixins.outset-ring__focus()输出统一的外接焦点环,保证与编辑后台其余控件的焦点样式一致; - 箭头图标 span(
components-external-link__icon)为inline-block、line-height: 1,行内起始侧留--wpds-dimension-padding-xs的间距,并使用默认字重(style.scss)。
由于颜色与间距完全由--wpds-*变量驱动,组件在 Gutenberg 的 Storybook/预览环境(浅色/深色主题)中会自动跟随主题变量取值,无需额外配置。
现状提示:Storybook 中已被标记为"不推荐"
需要注意一个当前仓库中的关键事实:在 Storybook 元数据里,ExternalLink的组件状态被标记为not-recommended,官方备注建议改用@wordpress/ui的Link组件并设置openInNewTabprop:
parameters: { componentStatus: { status: 'not-recommended', whereUsed: 'global', notes: 'Use `Link` from `@wordpress/ui` instead, with the `openInNewTab` prop set.', }, },(见 stories/index.story.tsx)
这意味着:存量代码中继续使用ExternalLink完全没问题,其锚点拦截、aria-label 等行为的测试基线依然有效;但新编写的 UI 代码按仓库当前指引应优先选择@wordpress/ui的Link。Storybook 中该组件仍提供Components/Navigation/ExternalLink故事(默认 args 为children: 'WordPress'、href: 'https://wordpress.org',见 stories/index.story.tsx),可用于观察其渲染效果与可访问性命名。
小结与速查
| 关注点 | 结论 | 依据 |
|---|---|---|
| 必填 props | children: ReactNode、href: string,其余透传至<a> | types.ts、README |
target不可覆盖 | 类型上Omit<..., 'target'>,内部固定_blank | index.tsx |
#开头的 href | 点击被preventDefault(),onClick仍会触发 | index.tsx |
| 可访问性名 | "链接文本 (opens in a new tab)",经 i18n 翻译 | index.tsx |
| 箭头方向 | LTR 为 ↗(U+2197),RTL 为 ↖(U+2196) | index.tsx |
| 样式令牌 | --wpds-color-foreground-interactive-brand等 WPDS 变量 | style.scss |
| 新代码建议 | 仓库内已标记 not-recommended,推荐@wordpress/ui的Link+openInNewTab | stories/index.story.tsx |
整体来看,ExternalLink的实现规模很小(单文件约 80 行),但把"新标签页外链"在编辑器环境里需要处理的边界问题——目标页强制、锚点例外、可访问性命名、表情替换豁免、RTL 方向——都收敛到了明确的源码位置,配合 4 个针对性 jsdom 测试,是一篇很适合精读的组件级参考实现。
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考