Dogtor

Serverless如何实现高级Markdown特性

从零开始实现提示框、表格和LaTeX数学公式的完整教程

全文共 2268 字,阅读约需 6 分钟

概述

在现代技术博客中,仅仅支持基础的Markdown语法已经不够了。本教程将带你一步步实现GitHub风格的高级Markdown特性,包括:

  • 🎯 GitHub风格提示框(NOTE、TIP、IMPORTANT、WARNING、CAUTION)
  • 📊 响应式表格渲染
  • 🧮 LaTeX数学公式支持
  • 🌙 深色模式适配
📝 NOTE
本教程假设你已经有一个基础的Next.js博客项目。如果还没有,建议先搭建基础博客框架。

第一步:项目依赖配置

安装必要依赖

首先,我们需要安装处理Markdown和数学公式的核心依赖:

npm install marked katex dompurify

依赖说明

包名作用版本建议
markedMarkdown解析器,支持GFM^12.0.2
katexLaTeX数学公式渲染^0.16.0+
dompurifyHTML内容安全过滤^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语法。

💡 TIP
这是一个TIP提示框,用于提供有用的建议和技巧。

❗ IMPORTANT
这是一个IMPORTANT提示框,用于强调关键信息。

⚠️ WARNING
这是一个WARNING提示框,用于警告潜在的问题。

🚨 CAUTION
这是一个CAUTION提示框,用于表示危险操作。

数学公式语法

行内公式:质能方程 是物理学的基础。

块级公式:

复杂公式(矩阵):

表格与公式结合

函数导数积分

实际效果展示

提示框效果

💡 TIP
我们选择marked而不是react-markdown是因为它更轻量且可控性更强。
0
💡 TIP
这是TIP提示框的实际效果,采用绿色主题,适合提供建议。

❗ IMPORTANT
这是IMPORTANT提示框的实际效果,采用紫色主题,适合强调重点。

⚠️ WARNING
这是WARNING提示框的实际效果,采用黄色主题,适合警告提示。

🚨 CAUTION
这是CAUTION提示框的实际效果,采用红色主题,适合危险警告。

数学公式效果

欧拉公式:

麦克斯韦方程组:

常见问题与解决方案

Q1: KaTeX公式不显示

💡 TIP
解决方案:
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 || ''}" />`;
};

总结

通过本教程,我们成功实现了:

  1. ✅ GitHub风格提示框 - 5种类型,支持深色模式
  2. ✅ LaTeX数学公式 - 行内和块级公式,客户端渲染
  3. ✅ 响应式表格 - 自动适配移动设备
  4. ✅ 深色模式支持 - 所有组件都有深色变体
  5. ✅ 性能优化 - 按需加载,避免不必要的资源浪费

💡 TIP
这套方案的优势在于:
- 轻量级:只加载必要的依赖
- 可扩展:容易添加新的Markdown特性
- 兼容性好:支持现代浏览器和移动设备
- 维护性强:代码结构清晰,易于调试

现在你就拥有了一个功能强大、视觉美观的技术博客系统!

评论

你的邮箱我们会严格保密,仅用于必要时的联系,不会在页面上公开显示。

表情直接显示在输入框中

请遵守相关法律法规,友善发言。垃圾广告、辱骂、违法违规内容将被删除。

Dogtor

人间札记,记录读书、琐事、思考,有烟火气,不硬核。

本站为个人博客,内容仅供学习和记录用途。

法律信息

© 2024-2026 博士狗的人间札记. 版权所有,未经许可请勿转载。

鄂ICP备2025135897号

本站总访问量:0次