跳到主要内容

超链接小组件更新,嵌入原生 Docusaurus

· 阅读需 14 分钟 ·
Castamere
热爱 Coding

demo

这次更新制作了一个悬浮窗动画效果,可以根据链接的不同类型渲染不同样式。其次是将部分功能封装,并嵌入了 Docusaurus 的 mdx <a> 标签,就可以在 markdown 文件中,使用 [text](url) 语法来调用

并且放到了 Github站外链接 上,欢迎大家使用和二创

缘起

在翻看 @wuanqin站外链接 的博客时,发现博主的超链接,会有一个悬浮的小框,显示外链/内链

uuanqin

顺带还有些功能也想实现,以及重构代码,就一起搞了

悬浮框

做这个悬浮框分两部分,一个是样式,一个是区分内外链接

内外链接判断

笔者把引用的链接分为了三种:

  • 站外链接
  • 站内文章
  • 页面内跳转

页面内跳转是,比如从 /blog/Link-update-2025-08 跳转到 /blog/Link-update-2025-08#内外链接判断,即跳到当前页面内的某一 part

站外文章最好判断,不以 / 开头的都算,其他就属于站内文章。判断是否是页面内跳转比较麻烦,首先判断链接内是否有 #;然后把 # 及之后的内容去掉,和当前路径做对比

链接类型判断
import { useLocation } from "@docusaurus/router";
const currentPath = useLocation().pathname;

const isExternalLink = !href.startsWith("/");
const isAnchorLink = href.includes("#") && href.split("#")[0] === currentPath;

但这样在 Docusaurus 中会有一个问题,useLocation 返回的是包括 i18n 在内的路径,也就是类似 /en/blog/ 这种,而我们在 markdown 里的路径就是一个纯净路径,是 Docusaurus 的多语言支持给我们正确添加 i18n 的

提示

这部分是 Docusaurus 的 @docusaurus/Link 组件的功能

举个栗子,如果使用 [超链接小组件](/blog/Link) 这样的语法,那么在英文版,会自动给我们添加 /en 的前缀跳转,这样避免语言间的混乱。但文中的 href 还是原始的 /blog/Link

所以笔者自制了一个 hook,用于在 Docusaurus 中获取当前的纯净路径。其实也不复杂,就是区别 defaultLocale 和 其他 Locale 的情况

usePlainLocation
import { useLocation } from "@docusaurus/router";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";

export function usePlainLocation(): string {
const { i18n } = useDocusaurusContext();
const currentPath = useLocation().pathname;

if (i18n.currentLocale === i18n.defaultLocale) {
return currentPath;
}

const localePrefix = `/${i18n.currentLocale}`;
if (currentPath.startsWith(localePrefix)) {
const plainPath = currentPath.slice(localePrefix.length);
return plainPath || "/";
}

return currentPath;
}

然后这样用就可以了

链接类型判断
import { usePlainLocation } from "@site/src/hooks/usePlainLocation";
const currentPath = useLocation().pathname;

const isExternalLink = !href.startsWith("/");
const isAnchorLink =
href.includes("#") && href.split("#")[0] === usePlainLocation();

悬浮框样式

想做一个类似聊天气泡,下面有个小尖尖那种,一点点来

基础样式

先让他显示出来:通过外层的 relative 来定位这个链接的宽度,再在中间显示出来,一个圆角矩形+一个小啾啾

return (
<span className="tailwind">
<span className="relative inline-block">
一个链接
<LinkBadge />
</span>
</span>
);
temp
一个链接站外链接
temp
LinkBadge
LinkBadge.tsx
import React from "react";
const LinkBadge: React.FC = () => {
return (
...
<span
className={classNames({
"absolute -top-8 left-1/2 transform -translate-x-1/2": true,
"px-2 py-1 text-xs rounded shadow-lg": true,
"pointer-events-none": true,
"whitespace-nowrap": true,
"text-white": true,
"bg-orange-500": true,
})}
>
站外链接
<span
className={classNames({
"absolute top-full left-1/2 transform -translate-x-1/2": true,
"border-l-4 border-r-4 border-t-4": true,
"w-0 h-0": true,
"border-transparent": true,
"border-t-orange-500": true,
})}
/>
</span>
...
);
};

export default LinkBadge;

渲染不同种类链接内容

第二步,根据不同类型的链接,渲染不同内容。把之前获取的 isExternalLink, isAnchorLink 传入

temp
一个链接站外链接123两个链接站内文章123三个链接页面内跳转
temp
LinkBadge.tsx
  import React from "react";
+ interface LinkBadgeProps {
+ isExternalLink: boolean;
+ isAnchorLink: boolean;
+ }

- const LinkBadge: React.FC = () => {
+ const LinkBadge: React.FC<LinkBadgeProps> = ({
+ isExternalLink,
+ isAnchorLink,
+ }) => {
return (
...
<span ... >
- 站外链接
+ {isExternalLink && 站外链接}
+ {isAnchorLink && 页面内跳转}
+ {!isExternalLink && !isAnchorLink && 站内文章}
<span ... />
</span>
...
);
};

export default LinkBadge;

上色

第三步给不同类型的链接加上不同的颜色

temp
一个链接站外链接123两个链接站内文章123三个链接页面内跳转
temp

为了方便之后加新的类型,将颜色放到一个单独的 color.ts 中

color.ts
interface BadgeColorScheme {
badgeColor: string;
arrowColor: string;
}

export const badgeColors: Record<string, BadgeColorScheme> = {
external: {
badgeColor: "bg-orange-500",
arrowColor: "border-t-orange-500",
},
internal: {
badgeColor: "bg-blue-500",
arrowColor: "border-t-blue-500",
},
inpage: {
badgeColor: "bg-violet-500",
arrowColor: "border-t-violet-500",
},
};

然后在组件中使用

LinkBadge.tsx
+ import { badgeColors } from "./colors";
...
const LinkBadge: React.FC<LinkBadgeProps> = ({
isExternalLink,
isAnchorLink,
}) => {

+ const type: keyof typeof badgeColors = isExternalLink
+ ? "external"
+ : isAnchorLink
+ ? "inpage"
+ : "internal";
+ const { badgeColor, arrowColor } = badgeColors[type];
return (
...
<span
className={classNames({
...
- "bg-orange-500": true,
+ [badgeColor]: true,
})}>
{isExternalLink && 站外链接}
{isAnchorLink && 页面内跳转}
{!isExternalLink && !isAnchorLink && 站内文章}
<span
className={classNames({
...
- "border-t-orange-500": true,
+ [arrowColor]: true,
})}/>
</span>
...
);
};

export default LinkBadge;

添加悬浮效果

在翻看 @wuanqin站外链接 的超链接时,发现他的悬浮效果应该就是单纯的 hoveropacity-100 ,不 hoveropacity-0。但这个样式会存在一定的割裂感

uuanqin

之前在制作这个超链接组件时,这一章站内文章详细介绍了笔者想要的效果

即:

不管是鼠标快速扫过还是悬浮不动,下划线会先全部拉满,再进行下一步的操作

针对这个悬浮框也一样,不管是鼠标快速扫过,还是悬浮到上面,都会完整地显示一次动画

demo

好消息是,hover 的逻辑之前已经实现过了,直接当一个参数传入用就行

提示

实现逻辑请参考这里站内文章

LinkBadge.tsx
  ...
interface LinkBadgeProps {
+ isVisible: boolean;
isExternalLink: boolean;
isAnchorLink: boolean;
}

const LinkBadge: React.FC<LinkBadgeProps> = ({
+ isVisible,
isExternalLink,
isAnchorLink,
}) => {
...

return (
<span
className={classNames({
+ "opacity-0 scale-75": !isVisible,
+ "opacity-100 scale-100": isVisible,
})}
>
...
</span>
);
};\

i18n

最后把 i18n 加上,对于不知道 docusaurus 的这个功能的朋友,请参考这里站内文章

LinkBadge.tsx
+ import Translate from "@docusaurus/Translate";
...
const LinkBadge: React.FC<LinkBadgeProps> = ({
..
return (
- {isExternalLink && 站外链接}
- {isAnchorLink && 页面内跳转}
- {!isExternalLink && !isAnchorLink && 站内文章}
+ {isExternalLink && <Translate>站外链接</Translate>}
+ {isAnchorLink && <Translate>页面内跳转</Translate>}
+ {!isExternalLink && !isAnchorLink && <Translate>站内文章</Translate>}
)
...
)

总之最后封装好的组件代码如下:

LinkBadge
LinkBadge.tsx
import Translate from "@docusaurus/Translate";
import classNames from "classnames";
import React from "react";
import { badgeColors } from "./colors";
interface LinkBadgeProps {
isVisible: boolean;
isExternalLink: boolean;
isAnchorLink: boolean;
}

const LinkBadge: React.FC<LinkBadgeProps> = ({
isVisible,
isExternalLink,
isAnchorLink,
}) => {
const type: keyof typeof badgeColors = isExternalLink
? "external"
: isAnchorLink
? "inpage"
: "internal";
const { badgeColor, arrowColor } = badgeColors[type];

return (
<span
className={classNames({
"absolute -top-8 left-1/2 transform -translate-x-1/2": true,
"px-2 py-1 text-xs rounded shadow-lg": true,
"transition-all duration-300": true,
"opacity-0 scale-75": !isVisible,
"opacity-100 scale-100": isVisible,
"pointer-events-none": true,
"whitespace-nowrap": true,
"text-white": true,
[badgeColor]: true,
})}
>
{isExternalLink && <Translate>站外链接</Translate>}
{isAnchorLink && <Translate>页面内跳转</Translate>}
{!isExternalLink && !isAnchorLink && <Translate>站内文章</Translate>}
<span
className={classNames({
"absolute top-full left-1/2 transform -translate-x-1/2": true,
"w-0 h-0": true,
"border-l-4 border-r-4 border-t-4": true,
"border-transparent": true,
"text-white": true,
[arrowColor]: true,
})}
/>
</span>
);
};

export default LinkBadge;

嵌入 Docusaurus

笔者想在 markdown 里直接使用 [text](url) 语法来调用这个组件,那就是直接用 swizzle

Swizzle <a>

提示

不知道 Docusaurus swizzle 的朋友可以看这里站外链接

简单来说就是修改 Docusaurus 的系统组件

首先把所有能 swizzle 的组件列出来

npm run swizzle -- --list | grep -i link
ComponentDescription
Footer/LinkItemThe footer link item component
Footer/LinksThe footer component rendering the footer links
Footer/Links/MultiColumnThe footer component rendering the footer links with a multi-column layout
Footer/Links/SimpleThe footer component rendering the footer links with a simple layout (single row)
MDXComponents/AThe component used to render <a> tags and Markdown links in MDX
SkipToContentThe component responsible for implementing the accessibility "skip to content" link (https://www.w3.org/TR/WCAG20-TECHS/G1.html站外链接)
BlogPostItem/Footer/ReadMoreLinkN/A

发现我们要改的是 MDXComponents/A 组件。使用如下命令 swizzle

npm run swizzle @docusaurus/theme-classic MDXComponents/A

会生成 ./src/theme/MDXComponents/A.tsx

./src/theme/MDXComponents/A.tsx
import Link from "@docusaurus/Link";
import type { Props } from "@theme/MDXComponents/A";
import React from "react";

export default function MDXA(props: Props): JSX.Element {
return <Link {...props} />;
}

接下来修改一下组件来适配

组件适配

从上面我们可以看到, Docusaurus Markdown 中的链接,会被渲染为 Docusaurus 的 <Link> 组件,其有以下优点

  • 自动检测 url 是否存在(站内 url)
  • 链接预加载
  • 自动应用 base URL
  • 智能添加 target="_blank" rel="noopener noreferrer"

你可以在这里站外链接找到它的详细介绍

但笔者之前的组件是从零开始一点点做的,不太好适配。所以就先把很多内容进行封装,然后分为两支,一个是原有的 Link 组件,完全自定义;另一个是强化了 Docusaurus 的 <Link> 组件,在后面会进行功能对比

首先把 Link 提取出来

DocusaurusMDXLinkEnhance.tsx
import Link from "@docusaurus/Link";
import type { Props } from "@theme/MDXComponents/A";
import React from "react";

const DocusaurusMDXLinkEnhance = (props: Props) => {
return (
<span>
<Link {...props} />
</span>
);
};

export default DocusaurusMDXLinkEnhance;

看了一下, 参数只需要 href 来判断内/外链,再研究一下 props,获取出来

DocusaurusMDXLinkEnhance.tsx
...
const DocusaurusMDXLinkEnhance = (props: Props) => {
+ const { href } = props;

return (
<span>
<Link {...props} />
</span>
);
};

具体封装的过程就不细讲了,总之最终版本如下:

LinkBadge
DocusaurusMDXLinkEnhance.tsx
import Link from "@docusaurus/Link";
import { useColorMode as useDocusaurusColorMode } from "@docusaurus/theme-common";
import { usePlainLocation } from "@site/src/hooks/usePlainLocation";
import type { Props } from "@theme/MDXComponents/A";
import classNames from "classnames";
import React from "react";
import {
colors,
LinkBadge,
UnderlineAnimation,
useHoverEffect,
} from "./_components";

const DocusaurusMDXLinkEnhance = (props: Props) => {
if (!props.href) return <Link {...props} />;
const { href } = props;

const { hover, leaving, onMouseEnter, onMouseLeave } = useHoverEffect();

let mode: "dark" | "light";
try {
mode = useDocusaurusColorMode().colorMode;
} catch {
mode = "dark";
}

const color = colors[mode];

const isExternalLink = !href.startsWith("/");
const isAnchorLink =
href.includes("#") && href.split("#")[0] === usePlainLocation();

return (
<span className="tailwind" style={{ display: "inline-block" }}>
<span
className={classNames({
"relative inline-block": true,
[color.main]: true,
[color.hover]: true,
})}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<Link {...props} />

<LinkBadge
isVisible={hover}
isExternalLink={isExternalLink}
isAnchorLink={isAnchorLink}
/>

<UnderlineAnimation
hover={hover}
leaving={leaving}
underlineColor={color.underScore}
/>
</span>
</span>
);
};

export default DocusaurusMDXLinkEnhance;

嵌入

然后改一下前面生成的 ./src/theme/MDXComponents/A.tsx,使用我们的组件即可

./src/theme/MDXComponents/A.tsx
- import Link from "@docusaurus/Link";
+ import DocusaurusMDXLinkEnhance from "@site/src/components/Link/DocusaurusMDXLinkEnhance";
import type { Props } from "@theme/MDXComponents/A";
import React from "react";

export default function MDXA(props: Props): JSX.Element {
- return <Link {...props} />;
+ return <DocusaurusMDXLinkEnhance {...props} />;
}

对比

代码放到了 Github站外链接 上,目前版本为 V1.1。如上面所说,分为两支。一个是原有 Link 组件,很多内容都可以自定义,另一个是 DocusaurusMDXLinkEnhance,仅用于加强 Markdown 中的使用

功能 \ 组件LinkDocusaurusMDXLinkEnhance
各种动画✔️✔️
设置是否显示悬浮框✔️(默认不显示)❌(默认显示)
Docusaurus 优化✔️
跳转逻辑自定义✔️
字体字号下划线自定义✔️
添加自定义颜色✔️

后记

后面可能考虑搞成一个 npm 包,但完了再研究吧

请作者喝可乐🥤:
本文遵循 CC 4.0 BY-SA站外链接 版权协议,转载请标明出处