前端解析markdown语法

现在有很多插件可以做到markdown解析,下面给大家介绍一下业务中常用的

主流 Markdown 解析库

  1. marked.js

    ​特点​​:轻量级(无依赖)、高性能(流式处理)、支持 CommonMark 和 GFM 规范。

    ​使用场景​​:快速解析静态内容或实时预览(如编辑器)。

    ​示例​​:

js 复制代码
     import { marked } from 'marked';
     const html = marked.parse('**Bold text**'); // 输出: <p><strong>Bold text</strong></p>
  1. markdown-it

    ​特点​​:高扩展性(插件生态)、支持自定义渲染规则(如表格、目录生成)。

    ​使用场景​​:复杂需求(数学公式、代码高亮、Vue 组件嵌入)。

    ​示例​​(配合代码高亮):

js 复制代码
   import markdownIt from 'markdown-it';
   import hljs from 'highlight.js';
   const md = markdownIt({ highlight: (code, lang) => hljs.highlight(code, { language: lang }).value });

markdown-it

我们项目使用的是markdown-it。它是一个功能强大的Markdown解析器,支持丰富的Markdown语法,可以将Markdown文本转换为HTML格式。它还支持各种配置选项和插件系统。

​安装​​:npm install markdown-it highlight.js

​配置选项​​:

js 复制代码
const md = new MarkdownIt({ 
   html:         false,        // 在源码中启用 HTML 标签
   xhtmlOut:     false,        // 使用 '/' 来闭合单标签 (比如 <br />)。
                               // 这个选项只对完全的 CommonMark 模式兼容。
   breaks:       false,        // 转换段落里的 '\n' 到 <br>。
   langPrefix:   'language-',  // 给围栏代码块的 CSS 语言前缀。对于额外的高亮代码非常有用。
   linkify:      false,        // 将类似 URL 的文本自动转换为链接。

   // 启用一些语言中立的替换 + 引号美化
   typographer:  false,

   // 双 + 单引号替换对,当 typographer 启用时。
   // 或者智能引号等,可以是 String 或 Array。

   // 比方说,你可以支持 '<<>>„"' 给俄罗斯人使用, '„"‚''  给德国人使用。
   // 还有 ['<<\xA0', '\xA0>>', '‹\xA0', '\xA0›'] 给法国人使用(包括 nbsp)。
   quotes: '""''',

   // 高亮函数,会返回转义的HTML。
   // 或 '' 如果源字符串未更改,则应在外部进行转义。
   // 如果结果以 <pre ... 开头,内部包装器则会跳过。
   highlight: function (/*str, lang*/) { return ''; }
});

​demo​​:

在公共组件markdown文件下创建copy.ts公共方法

ts 复制代码
// 复制方法
export const copyToClipboard = async (text: string) => {
 try {
   await navigator.clipboard.writeText(text);
   // 显示成功反馈
   console.log("复制成功!");
 } catch (err) {
   console.error("复制失败:", err);
   // 回退到document.execCommand方法
   const textarea = document.createElement("textarea");
   textarea.value = text;
   document.body.appendChild(textarea);
   textarea.select();
   document.execCommand("copy");
   document.body.removeChild(textarea);
   // 显示成功反馈
 }
};

创建highlight.ts

ts 复制代码
// https://github.com/highlightjs/highlight.js/tree/main/src/styles   highlight.js样式库
import hljs from "highlight.js";
// import "highlight.js/styles/github.css"; // GitHub 风格
import 'highlight.js/styles/atom-one-light.css' // Atom One Light 风格
// import 'highlight.js/styles/ascetic.css'


// 注册 Vue 语法
hljs.registerLanguage('vue', function(hljs) {
  return {
    subLanguage: 'xml',
    contains: [
      hljs.COMMENT('<!--', '-->', {
        relevance: 10
      }),
      {
        begin: /^(\s*)(<script>)/,
        end: /^(\s*)(<\/script>)/,
        subLanguage: 'javascript',
        excludeBegin: true,
        excludeEnd: true
      },
      {
        begin: /^(\s*)(<style(\s+scoped)?>)/,
        end: /^(\s*)(<\/style>)/,
        subLanguage: 'css',
        excludeBegin: true,
        excludeEnd: true
      }
    ]
  };
});

// 然后初始化 highlight.js
hljs.initHighlightingOnLoad();

export default hljs;

封装markdowm.vue组件

js 复制代码
<template>
  <div ref="containerRef" class="markdown-body" v-html="safeHtml" @click="handleClick" />
</template>

<script setup lang="ts">
import MarkdownIt from "markdown-it";
import DOMPurify from "dompurify";
import hljs from "./highlight";
import { ref, watch, nextTick } from "vue";
import { copyToClipboard } from "./copy";

const props = defineProps<{
  content: string;
}>();

const containerRef = ref<HTMLElement | null>(null);
const safeHtml = ref("");

// 渲染Markdown内容
const renderMarkdown = (content: string) => {
  const md = new MarkdownIt({
    html: true, // 允许HTML标签
    breaks: true, // 允许换行符
    linkify: true, // 允许链接
    typographer: true,
    highlight: (code: string, lang: string) => {
      // Base64编码特殊字符
      const base64Content = btoa(encodeURIComponent(code));

      if (lang && hljs.getLanguage(lang)) {
        try {
          return `<div class="markdown_header">
              <span>${lang}</span>
              <div class="copy-btn" data-copy="${base64Content}">复制</div>
            </div><pre class="hljs"><code>${
              hljs.highlight(code, {
                language: lang,
                ignoreIllegals: true
              }).value
            }</code></pre>`;
        } catch (__) {}
      }

      // 确保所有情况都有复制按钮
      return `
        <div class="markdown_header">
          <span>plaintext</span>
          <div class="copy-btn" data-copy="${base64Content}">复制</div>
        </div><pre class="hljs"><code>${md.utils.escapeHtml(code)}</code></pre>
      `;
    }
  }) as MarkdownIt;

  // 安全渲染方法
  return DOMPurify.sanitize(md.render(content));
};

// 使用watch确保内容渲染完成
watch(
  () => props.content,
  (newContent) => {
    safeHtml.value = renderMarkdown(newContent);

    // 在DOM更新后处理特殊状态
    nextTick(() => {
      if (containerRef.value) {
        containerRef.value.querySelectorAll(".copy-btn").forEach((btn) => {
          if (!btn.hasAttribute("data-copy")) {
            console.warn("未设置data-copy的按钮", btn);
          }
        });
      }
    });
  },
  { immediate: true }
);

// 事件委托处理(解决多实例问题)
const handleClick = (e: MouseEvent) => {
  const target = e.target as HTMLElement;
  const copyBtn = target.closest(".copy-btn");

  if (!copyBtn) return;
  if (!containerRef.value?.contains(copyBtn)) return;

  // 处理无属性的情况
  const base64Content = copyBtn.getAttribute("data-copy") || "";

  if (!base64Content) {
    console.error("复制按钮缺少data-copy属性", copyBtn);
    return;
  }

  // 解码
  const text = base64ToText(base64Content);
  copyToClipboard(text);

  // 视觉反馈
  const originalText = copyBtn.textContent;
  copyBtn.textContent = "已复制";
  setTimeout(() => {
    if (copyBtn.textContent === "已复制") {
      copyBtn.textContent = originalText;
    }
  }, 1500);
};

// Base64解码函数(安全处理特殊字符)
function base64ToText(base64: string) {
  try {
    return decodeURIComponent(atob(base64));
  } catch {
    // 尝试备选解码方案
    try {
      return decodeURIComponent(
        atob(base64)
          .split("")
          .map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
          .join("")
      );
    } catch {
      return base64; // 最终回退
    }
  }
}
</script>

<!-- 全局样式 -->
<style lang="scss">
.markdown-body {
  font-family: system-ui, sans-serif;
  line-height: 1.6;

  ul,
  ol {
    padding-left: 2em;
    margin: 0.8em 0;
  }

  table {
    border-collapse: collapse;
    margin: 1em 0;
    th,
    td {
      padding: 0.6em 1em;
      border: 1px solid #e5e7eb;
    }
  }
}
</style>

<style scoped lang="scss">
.markdown-body ::v-deep {
  h1,
  h2,
  h3 {
    margin: 1em 0;
  }

  .markdown_header {
    background-color: #ededed;
    border-radius: 10px 10px 0 0;
    padding: 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;

    .copy-btn {
      cursor: pointer;
      padding: 4px 8px;
      background-color: #f0f0f0;
      border-radius: 4px;
      font-size: 0.85em;
      user-select: none;
      transition: background-color 0.2s;

      &:hover {
        background-color: #e0e0e0;
      }

      // 添加点击效果
      &:active {
        transform: scale(0.95);
      }
    }
  }

  .hljs {
    //新增
    border: 1px solid #ededed;
    padding: 1rem;
    margin: 0;
  }
}
</style>

使用

js 复制代码
  <Markdown :content="message.content"></Markdown>

安全处理(XSS 防护)

解析库最好配合消毒库,避免恶意脚本注入:

​推荐库​​:DOMPurify

​功能​​:

  • 移除恶意脚本:删除或转义 HTML 中的 <script> 标签及其内容,防止执行恶意 JavaScript 代码。
  • 过滤不安全的属性:移除或转义 HTML 标签中的不安全属性,例如 onloadonclick 等事件处理器,这些属性可能被用来注入恶意代码。
  • 处理不安全的 URL:过滤掉可能指向恶意网站或包含恶意代码的 URL,例如 javascript: 协议的链接。
  • 自定义规则:允许开发者根据需要自定义允许或禁止的标签和属性,以满足特定的安全需求。

​安装​​:npm install dompurify

​示例​​:

js 复制代码
  import DOMPurify from 'dompurify';
  const safeHTML = DOMPurify.sanitize(marked.parse(userInput));
相关推荐
To_OC4 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic4 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy4 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志5 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble6 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯7 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜7 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司8 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g8 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js
江华森8 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http