前言
本程序员用 Astro 搭好了自己的小窝(博客),键盘敲得飞起,代码写得贼溜。写博客嘛,自然少不了秀一秀咱的“艺术品”——代码片段!用 Markdown 一渲染,哎哟,语法高亮整整齐齐,看着就倍儿舒坦,强迫症都治好了!
但是! 问题来了... 当读者想“偷师”我的代码(或者单纯想复制粘贴去debug),事情就尴尬了。指望原生 Markdown 给个“一键复制”按钮?它只会眨巴着无辜的大眼睛说:“臣妾做不到啊!🤷♂️” 这功能得跟 JS 老哥勾肩搭背才能整出来。
怎么办? 身为合格的程序员,第一反应当然是——封装它! 造个轮子,啊不,造个组件!让它替我们干这脏活累活。
噔噔噔噔! 主角光环闪瞎眼!MDX 大佬踩着七彩祥云(划掉)... 带着 JSX 的超能力,闪亮登场!✨ 它拍了拍 Markdown 的肩膀:“老弟,复制按钮这种小事,包在我身上!”
MDX介绍
MDX(Markdown + JSX)是一种结合 Markdown 的简洁语法与 JSX 动态能力的混合格式。它允许在 Markdown 文档中直接嵌入 React 组件(或其他框架组件),解决了传统 Markdown 无法实现交互性和动态内容的问题。
核心特性:
- Markdown 基础:支持标准 Markdown 语法(标题、列表、代码块等)。
- JSX 嵌入:可在 Markdown 中插入交互式组件(如按钮、图表、动态内容)。
- 组件复用:导入并复用自定义或第三方 UI 组件。
- Frontmatter 支持:通过 YAML 元数据管理页面属性(标题、日期等)。
- ESM 兼容:支持 JavaScript 模块导入(
import)。
说了这些,反正一句话MDX牛,强烈推荐在自己的静态博客中使用,替换之前的MD。
Astro中MDX的实践
作为目前当之无愧的最牛选手静态博客之王Astro,对MDX的支持度非常好,我们只需要安装一个插件@astrojs/mdx,集成进来就可以了,具体代码如下所示:
// astro.config.mjs文件
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import preact from "@astrojs/preact";
import { remarkReadingTime } from './remark-reading-time.mjs';
import tailwind from "@astrojs/tailwind";
import icon from "astro-icon";
// https://astro.build/config
export default defineConfig({
site: "http://localhost:4321",
prefetch: true,
markdown: {
shikiConfig: {
// 选择 Shiki 内置的主题(或添加你自己的主题)
// https://shiki.style/themes
theme: 'dracula',
// 另外,也提供了多种主题
// 查看下面关于使用亮/暗双主题的的说明
// themes: {
// light: 'github-light',
// dark: 'github-dark',
// },
// 添加自定义语言
// 注意:Shiki 内置了无数语言,包括 .astro!
// https://shiki.style/languages
langs: [],
// 启用自动换行,以防止水平滚动
wrap: true,
// 添加自定义转换器:https://shiki.style/guide/transformers
// 查找常用转换器:https://shiki.style/packages/transformers
transformers: []
},
gfm: true,
remarkPlugins: [remarkReadingTime]
},
integrations: [preact({
compat: true
}), mdx(), tailwind(), icon({
iconDir: "src/icons"
})], // 集成的相关插件
experimental: {
}
});
集成后,在MDX里面导入动态组件并使用就可以了,操作也是非常的简单。
实战例子
在 前言 中我们有一个代码块可一键复制的需求,现在我们就来实现它,然后在自己的Astro博客中导入使用。
设计一个这样的组件,我们就只需要考虑两点,展现代码块和复制代码两部分。
- 展现代码:将代码块转换为HTML代码,然后通过dangerouslySetInnerHTML属性去渲染HTML代码
- 复制代码:navigator.clipboard可以实现
既然是博客,自然是耶做了主题的切换,再添加了一个功能,用于检测主题的变化,然后切换代码块的主题。
获取主题
function getCurrentTheme() {
if (typeof window !== 'undefined') {
if (window.localStorage.getItem('theme')) {
return window.localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
}
return 'light';
}
将代码块转为HTML格式
const fetchHtml = async () => {
const html = await codeToHtml(code, {
lang: language,
theme: theme === 'dark' ? 'dracula' : 'github-light',
});
setHtml(html);
};
复制代码
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
setIsTip(true);
setTimeout(() => setIsTip(false), 2000);
});
};
以上就是主要功能的实现,现在只需要将其聚合在一起就可以封装成为我们想要的组件了,具体代码如下:
import type React from "preact/compat";
import { useEffect, useState } from "preact/compat";
import { codeToHtml } from 'shiki';
// 获取当前主题
function getCurrentTheme() {
if (typeof window !== 'undefined') {
if (window.localStorage.getItem('theme')) {
return window.localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
}
return 'light';
}
type Props = {
code: string;
language: string;
children?: React.ReactNode;
};
const CodeBlock = (props: Props) => {
const { code, language } = props;
const [html, setHtml] = useState('');
const [isTip, setIsTip] = useState(false);
const [theme, setTheme] = useState('light');
// 监听主题变化
useEffect(() => {
const updateTheme = () => setTheme(getCurrentTheme() || 'light');
updateTheme();
window.addEventListener('storage', updateTheme);
const mql = window.matchMedia('(prefers-color-scheme: dark)');
mql.addEventListener('change', updateTheme);
// 监听 class 变化(如 ThemeToggle 切换时)
const observer = new MutationObserver(() => {
updateTheme();
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
return () => {
window.removeEventListener('storage', updateTheme);
mql.removeEventListener('change', updateTheme);
observer.disconnect();
};
}, []);
// 根据主题渲染高亮
useEffect(() => {
const fetchHtml = async () => {
const html = await codeToHtml(code, {
lang: language,
theme: theme === 'dark' ? 'dracula' : 'github-light',
});
setHtml(html);
};
fetchHtml();
}, [code, language, theme]);
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
setIsTip(true);
setTimeout(() => setIsTip(false), 2000);
});
};
return (
<div
className={`relative group rounded-xl overflow-auto border border-gray-200 dark:border-gray-700 bg-gradient-to-br from-white/90 to-gray-100/80 dark:from-zinc-900/90 dark:to-zinc-800/80 shadow-lg transition-all duration-500 my-6 hover:scale-[1.01]`}
style={{ fontSize: '1em', boxShadow: '0 4px 24px 0 rgba(0,0,0,0.08)' }}
onClick={() => copyToClipboard(code)}
>
<button
className="absolute right-5 top-2 text-xs opacity-90 group-hover:opacity-100 bg-white/90 dark:bg-zinc-800 px-3 py-1 rounded-full shadow border border-gray-300 dark:border-zinc-700 transition-all hover:bg-blue-100 hover:text-blue-600 dark:hover:bg-zinc-700 dark:hover:text-yellow-300 text-gray-700 dark:text-gray-200 font-semibold"
style={{ zIndex: 2 }}
type="button"
tabIndex={-1}
onClick={e => { e.stopPropagation(); copyToClipboard(code); }}
>
<span className="inline-block align-middle mr-1">📋</span>复制
</button>
{isTip && (
<span className="absolute right-5 top-10 text-xs text-green-600 dark:text-green-400 opacity-90 bg-white/90 dark:bg-zinc-900/90 px-3 py-1 rounded-full shadow border border-green-200 dark:border-green-700 transition-all" style={{ zIndex: 2 }}>已复制!</span>
)}
<div
className="whitespace-pre-wrap break-words px-5 py-4 overflow-x-auto font-mono text-[0.98em] leading-relaxed selection:bg-blue-200 dark:selection:bg-blue-900/60"
style={{ minHeight: 40 }}
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
);
};
export default CodeBlock;
然后将其组件导入MDX中
import CodeBlock from "@components/CodeBlock";
### 内网穿透 - 端口映射
**局部安装**:
<CodeBlock
code={
`npm install -g localtunnel\nlt --port 端口号`
}
language="javascript"
client:load
/>
**全局安装**:
<CodeBlock
code={ `npx localtunnel --port 端口号`}
language="javascript"
client:load
/>
### NPM 包更新
<CodeBlock
code={ `npm install -g npm-check-updates\nncu -u\nnpm install`}
language="javascript"
client:load
/>
展现格式如下:
总结
回顾本文的探讨,通过一个代码块一键复制的需要,我们了解到了MDX,并感受到了MDX的强大,让我们的静态博客也能动起来,创建出属于我们程序员的优美博客。