不必东奔西跑,一篇搞定Vue3集成markdown编辑器+highlight代码高亮+LaTex(mathJax)数学公式!

序言

在现代Web开发中,Markdown支持已经成为许多应用不可或缺的功能,尤其是在文档编写、技术博客、知识管理等领域,特别是最近大火的deepseek大语言模型,其UI界面更需要支持Markdown语法的解析。而为了提升用户体验,我们通常还需要集成代码高亮和数学公式的显示功能。今天,我们将通过一个Vue3组件,一次性搞定这些需求。


一、功能概述

本篇将实现以下功能:

  1. 动态渲染Markdown内容
  2. 代码块高亮(全自动支持多种语言)
  3. LaTex数学公式渲染
  4. 一键复制代码

二、核心代码解析

1、安装依赖

首先,我们先给项目安装以下依赖,后面两个AntDesign Vue是为了弹出提示用的,最后的less是我用的比较习惯,动手能力强的读者可以换成Scss或者原生css。

npm i marked
npm i marked-highlight
npm i github-markdown-css
npm i highlight.js
npm i ant-design-vue
npm i @ant-design/icons-vue
npm i less -D

2、调整mathjax的引用方式

因为mathjax比较古老,不支持import方式引入,所以接下来找到node_modules下的mathjax目录,把它拷贝到public下面
在这里插入图片描述
既然已经拷贝过去了,那么我们就可以在index.html里引用它了

<!doctype html>
<html>
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>项目的标题</title>
    <!-- 重点是下面这行 -->
    <script src="/mathjax/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
    
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

3、写一个公式渲染工具

在src/util目录下面加上Mathjax.js文件,这样当需要渲染的时候调用它就可以了

let isMathjaxConfig = false;
const initMathjaxConfig = () => {
    if (!window.MathJax) {
        return;
    }
    window.MathJax = {
        tex: {
            inlineMath: [
                ["$", "$"],
                ["\\(", "\\)"],
            ], // ⾏内公式选择符
            displayMath: [
                ["$$", "$$"],
                ["\\[", "\\]"],
            ], // 段内公式选择符
        },
        options: {
            skipHtmlTags: [
                "script",
                "noscript",
                "style",
                "textarea",
                "pre",
                "code",
                "a",
            ], // 避开某些标签
            ignoreHtmlClass: "tex2jax_ignore",
            processHtmlClass: "tex2jax_process",
        },
    };
    isMathjaxConfig = true;
};
const TypeSet = async function (elementId) {
    if (!window.MathJax) {
        return;
    }
    window.MathJax.startup.promise = window.MathJax.startup.promise
        .then(() => {
            return window.MathJax.typesetPromise();
        })
        .catch((err) => console.log("Typeset failed: " + err.message));
    return window.MathJax.startup.promise;
};
export default {
    isMathjaxConfig,
    initMathjaxConfig,
    TypeSet,
};

4、核心渲染组件

接下来我们写一个Markdown.vue,把这些功能都写出来。

<template>
  <div v-html="htmlContent" class="markdown-body"></div>
</template>

<script>
import {defineComponent, nextTick, onMounted, ref, watch} from 'vue';
import {Marked} from 'marked';
import {markedHighlight} from 'marked-highlight';
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';
// 不喜欢Ant Design Vue,可以不用它
import {message} from 'ant-design-vue';
import 'github-markdown-css/github-markdown-light.css';
import mathJax from "@/util/MathJax";

let doCopy = function (e) {
  const code = this.parentNode.parentNode.querySelector('code');
  navigator.clipboard
      .writeText(code.innerText)
      .then(() => {
        // 用Ant Design Vue 进行提示,不喜欢可以换掉
        message.success('复制成功');
      })
      .catch((error) => {
        // 用Ant Design Vue 进行提示,不喜欢可以换掉
        message.error('复制失败');
      });
}

export default defineComponent({
  name: 'DynamicMarkdown',
  props: {
    value: {
      type: String,
      required: true,
    },
  },
  setup(props) {
    const htmlContent = ref('');

    // 初始化 marked 实例
    const marked = new Marked(
        markedHighlight({
          async: false,
          langPrefix: 'language-',
          emptyLangClass: 'no-lang',
          highlight: (code, language) => {
            // 代码高亮自动识别语言的关键就在这里
            return hljs.highlightAuto(code, [language]).value;
          },
        })
    );

    // 增强代码块功能
    const enhanceCodeBlock = (content) => {
      return content.replace(/<pre><code/g, '<pre><div class="enhance"><div class="copyCode">复制</div></div><code');
    };

    // 复制功能绑定
    const bindCopyFunction = (el) => {
      const codeBlocks = el.querySelectorAll('pre');
      codeBlocks.forEach((codeBlock) => {
        const enhance = codeBlock.querySelector('.enhance');
        if (enhance) {
          const copyCode = enhance.querySelector('.copyCode');
          if (copyCode) {
            copyCode.removeEventListener('click', doCopy);
            copyCode.addEventListener('click', doCopy);
          }
        }
      });
    };

    // 解析 Markdown 内容
    const parseMarkdown = () => {
      htmlContent.value = props.value
      // 这里是关键,很多人就折在这了,时序上一定是先转公式,再取出来转Markdown,最后转代码
      nextTick(()=>{
        // 先转公式
        if (mathJax) {
          if (mathJax.isMathjaxConfig) {
            mathJax.initMathjaxConfig()
          }
          mathJax.TypeSet()
        }
        nextTick(()=>{
          htmlContent.value=document.querySelector('.markdown-body').innerHTML
          htmlContent.value = enhanceCodeBlock(marked.parse(htmlContent.value));
          nextTick(() => {
            const el = document.querySelector('.markdown-body');
            if (el) {
              bindCopyFunction(el);
            }
          });
        })
      })

    };

    // 监听 value 变化
    watch(
        () => props.value,
        (newValue, oldValue) => {
          if (newValue) {
            parseMarkdown();
          }
        },
        {immediate: true}
    );

    onMounted(() => {
      if (props.value) {
        parseMarkdown();
      }
    });

    return {
      htmlContent,
    };
  },
});
</script>
<style lang="less">
.markdown-body {
  padding: 20px;
  box-sizing: border-box;
}

pre {
  position: relative;
}

pre .enhance {
  display: flex;
  color: #247aaa;
  padding: 10px;
  box-sizing: border-box;
  font-size: 12px;
  border-radius: 9px;
  justify-content: flex-end;
  //background-color: #202020;
  position: absolute;
  top: 0;
  right: 0;

  .copyCode {
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;

    &:hover {
      color: rgba(2, 120, 255, 0.84);
    }

    i {
      font-size: 16px;
      margin-left: 5px;
    }
  }
}
.markdown-body code, .markdown-body tt{
  background-color: #ffe6e6;
  color: #df3b3b;
}
</style>

5、调用方示例

为了方便调试,左侧屏幕可以输入markdown,右侧可以实时显示出效果

<template>
  <div style="display: flex;height: 100%">
    <textarea v-model="value" style="width: 30%;flex-shrink: 0;height: 100%"></textarea>
    <Markdown v-model:value="value" style="width: 100%;overflow-y: scroll"></Markdown>
  </div>
</template>
<script>
import Markdown from '@/components/Markdown.vue'

export default {
  name: 'MarkdownView',
  components: {
    Markdown
  },
  data() {
    return {
      value: ""
    }
  }
};
</script>

三、运行效果

运行效果如下,很完美的还原了公式,并高亮了代码
在这里插入图片描述
在这里插入图片描述

四、深夜模式改造

如果白底黑子看的很刺眼,也可以很方便的改为深夜模式,改这两行即可:
在这里插入图片描述
然后我们就可以得到一个如五彩斑斓黑一样的界面了:
在这里插入图片描述


总结

通过以上实现,我们成功地在Vue3中集成了Markdown渲染、代码高亮和数学公式支持。这个组件不仅可以满足基本的Markdown需求,还提供了额外的代码复制按钮。

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值