在构建技术博客时,我们通常面临一个两难选择:是追求极致的静态内容写作体验(Markdown),还是追求丰富的交互能力(React 页面)?
最近,我对本站的 Labs 系统进行了重构。目标很简单:我希望像写博客一样轻松地创建小工具(如 JSON 格式化器),同时这些工具不仅能独立运行,还能直接“嵌入”到任何技术文章中作为演示。
本文将介绍这一架构的实现细节,以及其中蕴含的设计模式。
痛点:内容与逻辑的耦合
在重构之前,本站的 "Labs" 是硬编码的 Next.js 页面。比如 json-formatter,它是一个完整的 page.tsx,包含了 UI 布局、标题描述和核心逻辑。
这种方式有两个问题:
- 复用困难:如果我想在一篇关于 "JSON 解析原理" 的文章中插入这个格式化器演示,我必须复制粘贴代码,或者搞得很复杂的 iframe。
- 维护成本:修改工具的说明文案需要改代码,而不是改 Markdown。
解决方案:MDX + 组件注入
我们采用了 MDX (Markdown + JSX) 作为核心媒介。MDX 允许我们在 Markdown 文档中直接导入和使用 React 组件。结合 Next.js 的 Server Components,我们可以实现高性能的混合渲染。
1. 架构概览
我们将系统拆分为三个部分:
- 数据层 (Content):
.mdx文件,只负责文案和“调用”组件。 - 逻辑层 (Components): 纯粹的 React 组件,负责交互逻辑。
- 渲染层 (Renderer): 负责将组件注入到 MDX 中。
2. 实现细节
第一步:提取纯逻辑组件
首先,我们将业务逻辑从页面中剥离,封装成独立的客户端组件。
// apps/web/components/labs/json-formatter.tsx
"use client";
export function JsonFormatter() {
const [input, setInput] = useState("");
// ... 核心逻辑
return (
<div className="json-formatter-ui">
{/* UI 渲染 */}
</div>
);
}
第二步:建立组件注册表
我们需要一个“桥梁”来告诉 MDX 引擎,当它遇到 <JsonFormatter /> 标签时应该渲染什么。
// apps/web/components/mdx-components.tsx
import { JsonFormatter } from "@/components/labs/json-formatter";
export const mdxComponents = {
JsonFormatter,
// 未来可以在这里注册更多组件
};
第三步:MDX 内容驱动
现在,我们可以像写文章一样写工具了。在 content/labs/json-formatter.mdx 中:
---
title: "JSON Formatter"
summary: "A simple tool to format JSON."
---
这里是工具的介绍文案...
<JsonFormatter />
### 原理解析
这个工具使用了 json-bigint 库...
第四步:渲染与注入
在 Next.js 的页面组件中,我们使用 next-mdx-remote 并通过 components 属性注入我们的注册表。
// apps/web/app/labs/feature/[slug]/page.tsx
import { MDXRemote } from "next-mdx-remote/rsc";
import { mdxComponents } from "@/components/mdx-components";
export default async function LabPage({ params }) {
const post = await getLabData(params.slug);
return (
<article className="prose">
<h1>{post.title}</h1>
{/* 关键点:依赖注入 */}
<MDXRemote
source={post.content}
components={mdxComponents}
/>
</article>
);
}
设计模式分析
这一重构过程中,体现了几个经典的设计模式:
1. 关注点分离 (Separation of Concerns)
- 内容创作者(即使是我自己)只需要关注 Markdown 文案和组件的“位置”。
- 组件开发者 只需要关注组件内部的交互逻辑(Input/Output),不需要关心它被放在哪里(独立页面还是文章中间)。
2. 依赖注入 (Dependency Injection)
MDXRemote 组件本身并不知道 <JsonFormatter> 是什么。我们通过 components 属性,在运行时将具体的组件实现“注入”给渲染引擎。这使得我们可以随时替换组件实现,或者在不同的上下文(如移动端 APP)中注入不同的原生组件,而无需修改 MDX 源文件。
3. 组合模式 (Composition)
MDX 本质上是将文档视作组件树。我们不是在写一个巨大的页面,而是通过组合小的、可复用的部分(标题、段落、交互组件)来构建页面。
总结
通过这种架构,本站的 "Labs" 不再是孤立的页面,而是可流动的“资源”。我可以在任何一篇文章中,通过简单的 <ComponentName /> 语法,唤起一个复杂的交互式演示。
这正是现代 Web 开发的魅力:内容即数据,组件即逻辑,渲染即组合。
附
嵌入到本文章的小组件"JSON 格式化器":