Serverless如何实现高级Markdown特性
从零开始实现提示框、表格和LaTeX数学公式的完整教程
全文共 2268 字,阅读约需 6 分钟
概述
在现代技术博客中,仅仅支持基础的Markdown语法已经不够了。本教程将带你一步步实现GitHub风格的高级Markdown特性,包括:
- 🎯 GitHub风格提示框(NOTE、TIP、IMPORTANT、WARNING、CAUTION)
- 📊 响应式表格渲染
- 🧮 LaTeX数学公式支持
- 🌙 深色模式适配
NOTE
本教程假设你已经有一个基础的Next.js博客项目。如果还没有,建议先搭建基础博客框架。
第一步:项目依赖配置
安装必要依赖
首先,我们需要安装处理Markdown和数学公式的核心依赖:
npm install marked katex dompurify
依赖说明
| 包名 | 作用 | 版本建议 |
|---|---|---|
marked | Markdown解析器,支持GFM | ^12.0.2 |
katex | LaTeX数学公式渲染 | ^0.16.0+ |
dompurify | HTML内容安全过滤 | ^3.2.6 |
TIP
我们选择
marked而不是react-markdown是因为它更轻量且可控性更强。第二步:配置KaTeX样式
在_document.js中添加KaTeX CSS
创建或编辑pages/_document.js文件:
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html>
<Head>
{/* KaTeX CSS */}
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css"
integrity="sha384-GvrOXuhMATgEsSwCs4smul74iXGOixntILdUW9XmUC6+HX0sLNAK3q71HotJqlAn"
crossOrigin="anonymous"
/>
{/* KaTeX JavaScript */}
<script
defer
src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js"
integrity="sha384-cpW21h6RZv/phavutF+AuVYrr+dA8xD9zs6FwLpaCct6O9ctzYFfFr4dgmgccOTx"
crossOrigin="anonymous"
></script>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
WARNING
不要在组件的
next/head中添加样式表,这会导致Next.js警告。正确的做法是在_document.js中添加。第三步:创建Markdown渲染组件
ContentSection组件实现
创建components/ContentSection/index.js:
import React, { useEffect, useState, useRef } from "react";
import { marked } from "marked";
const ContentSection = ({ content }) => {
const [htmlContent, setHtmlContent] = useState("");
const containerRef = useRef(null);
useEffect(() => {
if (content) {
// 配置marked选项
marked.setOptions({
gfm: true, // 启用GitHub Flavored Markdown
breaks: true, // 支持换行
headerIds: false, // 禁用标题ID避免冲突
});
// 自定义表格渲染
const renderer = new marked.Renderer();
// 表格渲染
renderer.table = function(header, body) {
return `<table class="markdown-table">{body}</table>`;
};
renderer.tablerow = function(content) {
return `<tr>${content}</tr>`;
};
renderer.tablecell = function(content, flags) {
const type = flags.header ? 'th' : 'td';
const align = flags.align ? ` align="${flags.align}"` : '';
return `<{align}>{type}>`;
};
// 数学公式处理 - 先保护公式不被marked处理
let processedContent = content;
const mathStore = [];
// 保护块级数学公式
processedContent = processedContent.replace(/\([\s\S]*?)\/g, (match, formula) => {
const id = mathStore.length;
mathStore.push({ type: 'block', formula: formula.trim() });
return `%%MATHBLOCK${id}%%`;
});
// 保护行内数学公式
processedContent = processedContent.replace(/\\r\n]+?)\$/g, (match, formula) => {
const id = mathStore.length;
mathStore.push({ type: 'inline', formula: formula.trim() });
return `%%MATHINLINE${id}%%`;
});
// 处理GitHub风格的提示框
processedContent = processedContent.replace(
/^>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*\n((?:>.*(?:\n|$))*)/gim,
(match, type, content) => {
const cleanContent = content.replace(/^>\s?/gm, '').trim();
const typeClass = type.toLowerCase();
const icons = {
note: '📝', tip: '💡', important: '❗',
warning: '⚠️', caution: '🚨'
};
const icon = icons[typeClass] || '📝';
const processedAlertContent = marked(cleanContent);
return `<div class="markdown-alert markdown-alert-${typeClass}">
<div class="markdown-alert-title">
<span class="markdown-alert-icon">${icon}</span>
${type}
</div>
<div class="markdown-alert-content">${processedAlertContent}</div>
</div>`;
}
);
marked.use({ renderer });
let html = marked(processedContent);
// 恢复数学公式为等待KaTeX渲染的元素
mathStore.forEach((item, id) => {
if (item.type === 'block') {
html = html.replace(`%%MATHBLOCK${id}%%`,
`<div class="katex-block" data-latex="${encodeURIComponent(item.formula)}"></div>`
);
} else {
html = html.replace(`%%MATHINLINE${id}%%`,
`<span class="katex-inline" data-latex="${encodeURIComponent(item.formula)}"></span>`
);
}
});
setHtmlContent(html);
}
}, [content]);
// 渲染KaTeX公式
useEffect(() => {
if (htmlContent && containerRef.current) {
const renderMath = () => {
if (typeof window !== 'undefined' && window.katex) {
renderWithKaTeX(window.katex);
} else {
import('katex').then((katex) => {
renderWithKaTeX(katex.default || katex);
}).catch(console.error);
}
};
function renderWithKaTeX(katex) {
// 渲染块级公式
const blockElements = containerRef.current.querySelectorAll('.katex-block');
blockElements.forEach((element) => {
const latex = decodeURIComponent(element.dataset.latex);
try {
katex.render(latex, element, {
displayMode: true,
throwOnError: false,
output: 'html',
trust: true
});
} catch (e) {
element.textContent = '';
}
});
// 渲染行内公式
const inlineElements = containerRef.current.querySelectorAll('.katex-inline');
inlineElements.forEach((element) => {
const latex = decodeURIComponent(element.dataset.latex);
try {
katex.render(latex, element, {
displayMode: false,
throwOnError: false,
output: 'html',
trust: true
});
} catch (e) {
element.textContent = '';
}
});
}
renderMath();
}
}, [htmlContent]);
return (
<div
ref={containerRef}
className="markdown-class"
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
);
};
export default ContentSection;
IMPORTANT
注意数学公式的处理顺序:先提取保护,再处理其他markdown语法,最后渲染数学公式。这样可以避免marked误解析数学符号。
第四步:添加CSS样式
在styles/markdown.css中添加样式
/* GitHub风格提示框样式 */
.markdown-class .markdown-alert {
@apply my-4 p-4 border-l-4 rounded-lg shadow-sm;
}
.markdown-class .markdown-alert-title {
@apply flex items-center gap-2 font-semibold mb-2 text-sm uppercase tracking-wide;
}
.markdown-class .markdown-alert-icon {
@apply text-lg;
}
.markdown-class .markdown-alert-content {
@apply opacity-90 leading-relaxed;
}
/* NOTE 提示框 - 蓝色主题 */
.markdown-class .markdown-alert-note {
@apply bg-blue-50 border-blue-400 dark:bg-blue-900/20 dark:border-blue-500;
}
.markdown-class .markdown-alert-note .markdown-alert-title {
@apply text-blue-800 dark:text-blue-300;
}
.markdown-class .markdown-alert-note .markdown-alert-content {
@apply text-blue-800 dark:text-blue-200;
}
/* TIP 提示框 - 绿色主题 */
.markdown-class .markdown-alert-tip {
@apply bg-green-50 border-green-400 dark:bg-green-900/20 dark:border-green-500;
}
.markdown-class .markdown-alert-tip .markdown-alert-title {
@apply text-green-800 dark:text-green-300;
}
.markdown-class .markdown-alert-tip .markdown-alert-content {
@apply text-green-800 dark:text-green-200;
}
/* IMPORTANT 提示框 - 紫色主题 */
.markdown-class .markdown-alert-important {
@apply bg-purple-50 border-purple-400 dark:bg-purple-900/20 dark:border-purple-500;
}
.markdown-class .markdown-alert-important .markdown-alert-title {
@apply text-purple-800 dark:text-purple-300;
}
.markdown-class .markdown-alert-important .markdown-alert-content {
@apply text-purple-800 dark:text-purple-200;
}
/* WARNING 提示框 - 黄色主题 */
.markdown-class .markdown-alert-warning {
@apply bg-yellow-50 border-yellow-400 dark:bg-yellow-900/20 dark:border-yellow-500;
}
.markdown-class .markdown-alert-warning .markdown-alert-title {
@apply text-yellow-800 dark:text-yellow-300;
}
.markdown-class .markdown-alert-warning .markdown-alert-content {
@apply text-yellow-800 dark:text-yellow-200;
}
/* CAUTION 提示框 - 红色主题 */
.markdown-class .markdown-alert-caution {
@apply bg-red-50 border-red-400 dark:bg-red-900/20 dark:border-red-500;
}
.markdown-class .markdown-alert-caution .markdown-alert-title {
@apply text-red-800 dark:text-red-300;
}
.markdown-class .markdown-alert-caution .markdown-alert-content {
@apply text-red-800 dark:text-red-200;
}
/* 响应式表格样式 */
.markdown-class .markdown-table,
.markdown-class table {
@apply w-full my-5 border-collapse border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden shadow-md;
}
.markdown-class .markdown-table th,
.markdown-class table th {
@apply bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 px-4 py-2 text-left font-semibold text-gray-900 dark:text-gray-100;
}
.markdown-class .markdown-table td,
.markdown-class table td {
@apply border border-gray-300 dark:border-gray-600 px-4 py-2 text-gray-800 dark:text-gray-200;
}
.markdown-class .markdown-table tr:nth-child(even),
.markdown-class table tr:nth-child(even) {
@apply bg-gray-50 dark:bg-gray-800;
}
.markdown-class .markdown-table tr:hover,
.markdown-class table tr:hover {
@apply bg-gray-100 dark:bg-gray-700;
}
/* KaTeX数学公式样式 */
.markdown-class .katex {
@apply text-gray-900 dark:text-gray-100;
}
.markdown-class .katex-display {
@apply my-4;
}
TIP
使用Tailwind的
@apply指令可以让CSS更简洁,同时保持良好的可维护性。第五步:使用示例
提示框语法
NOTE
这是一个NOTE提示框,用于显示重要的补充信息。
可以包含多行内容,支持粗体和斜体等基础markdown语法。
可以包含多行内容,支持粗体和斜体等基础markdown语法。
TIP
这是一个TIP提示框,用于提供有用的建议和技巧。
IMPORTANT
这是一个IMPORTANT提示框,用于强调关键信息。
WARNING
这是一个WARNING提示框,用于警告潜在的问题。
CAUTION
这是一个CAUTION提示框,用于表示危险操作。
数学公式语法
行内公式:质能方程 是物理学的基础。
块级公式:
复杂公式(矩阵):
表格与公式结合
| 函数 | 导数 | 积分 |
|---|---|---|
实际效果展示
提示框效果
TIP
我们选择
marked而不是react-markdown是因为它更轻量且可控性更强。
TIP
这是TIP提示框的实际效果,采用绿色主题,适合提供建议。
IMPORTANT
这是IMPORTANT提示框的实际效果,采用紫色主题,适合强调重点。
WARNING
这是WARNING提示框的实际效果,采用黄色主题,适合警告提示。
CAUTION
这是CAUTION提示框的实际效果,采用红色主题,适合危险警告。
数学公式效果
欧拉公式:
麦克斯韦方程组:
常见问题与解决方案
Q1: KaTeX公式不显示
TIP
解决方案:
1. 检查KaTeX CSS是否正确加载
2. 确认公式语法正确
3. 查看浏览器控制台是否有JS错误
1. 检查KaTeX CSS是否正确加载
2. 确认公式语法正确
3. 查看浏览器控制台是否有JS错误
Q2: 提示框内的Markdown不渲染
WARNING
原因:提示框内容需要单独经过marked处理。
解决方案:在处理提示框时,对内容使用
解决方案:在处理提示框时,对内容使用
marked(cleanContent)进行二次渲染。Q3: 深色模式下样式异常
IMPORTANT
解决方案:确保所有颜色都有对应的dark模式变体,使用Tailwind的
dark:前缀。性能优化建议
1. 按需加载KaTeX
// 只在有数学公式时才加载KaTeX
const hasMath = content.includes('$');
if (hasMath) {
// 加载KaTeX
}
2. 缓存渲染结果
// 可以考虑在构建时预渲染markdown内容
// 减少客户端渲染压力
3. 图片懒加载
// 为markdown中的图片添加懒加载
renderer.image = function(href, title, text) {
return `<img loading="lazy" src="{text}" title="${title || ''}" />`;
};
总结
通过本教程,我们成功实现了:
- ✅ GitHub风格提示框 - 5种类型,支持深色模式
- ✅ LaTeX数学公式 - 行内和块级公式,客户端渲染
- ✅ 响应式表格 - 自动适配移动设备
- ✅ 深色模式支持 - 所有组件都有深色变体
- ✅ 性能优化 - 按需加载,避免不必要的资源浪费
TIP
这套方案的优势在于:
- 轻量级:只加载必要的依赖
- 可扩展:容易添加新的Markdown特性
- 兼容性好:支持现代浏览器和移动设备
- 维护性强:代码结构清晰,易于调试
- 轻量级:只加载必要的依赖
- 可扩展:容易添加新的Markdown特性
- 兼容性好:支持现代浏览器和移动设备
- 维护性强:代码结构清晰,易于调试
现在你就拥有了一个功能强大、视觉美观的技术博客系统!
