前端解析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));
相关推荐
suedar3 分钟前
关于工程化的随想
前端
安琪吖12 分钟前
微前端:qiankun框架在开发中遇到的问题
前端·vue·element-ui
不爱说话郭德纲15 分钟前
🔥产品:"这功能很常见,不用原型,参考竞品就行!" 你会怎么做
前端·产品经理·产品
wordbaby23 分钟前
React 异步请求数据处理优化经验总结
前端·react.js
拉不动的猪25 分钟前
回顾 pinia VS vuex
前端·vue.js·面试
Warren9830 分钟前
Java异常讲解
java·开发语言·前端·javascript·vue.js·ecmascript·es6
超级土豆粉1 小时前
Taro Hooks 完整分类详解
前端·javascript·react.js·taro
iphone1081 小时前
从零开始学网页开发:HTML、CSS和JavaScript的基础知识
前端·javascript·css·html·网页开发·网页
2503_928411561 小时前
7.31 CSS-2D效果
前端·css·css3
辰九九1 小时前
Vue响应式原理
前端·javascript·vue.js