md-editor-v3中使用IntersectionObserver监听标题的注意事项

md-editor-v3中使用IntersectionObserver监听标题的注意事项

在开发基于md-editor-v3的Markdown编辑器时,实现目录导航功能是一个常见需求。其中使用IntersectionObserver API来监听标题元素的可见性变化是一种优雅的解决方案。然而,在实际开发中,开发者可能会遇到一个棘手的问题:当Markdown内容包含代码块时,IntersectionObserver会失效。

问题根源分析

这个问题的本质在于md-editor-v3的渲染机制。Markdown内容在渲染过程中会经历多次编译:

  1. 初次解析Markdown语法为HTML
  2. 代码高亮处理(当内容包含代码块时)
  3. 最终渲染输出

当内容不包含代码块时,highlight.js库不会触发重新渲染,标题元素保持稳定,IntersectionObserver可以正常工作。但是当内容包含代码块时,highlight.js会进行二次编译,导致DOM节点被替换,原先被监听的标题节点变成了"过时"的节点,从而使得IntersectionObserver失效。

解决方案

md-editor-v3提供了onHtmlChanged事件,我们可以利用这个事件在HTML内容变化后重新建立监听。以下是实现步骤:

  1. 初始化标题信息:在组件挂载时,收集所有标题元素并初始化状态
  2. 建立IntersectionObserver:创建一个IntersectionObserver实例来监听标题元素的可见性变化
  3. 响应HTML变化:通过onHtmlChanged事件在内容更新后重新建立监听
  4. 确保DOM更新完成:使用nextTick()确保在DOM更新完成后再进行操作

完整实现代码

<template>
  <div>
    <MdPreview
      v-model="content"
      editorId="editorId-preview"
      :mdHeadingId="mdHeadingId"
      :theme="themeStore.theme"
      previewTheme="smart-blue"
      @onHtmlChanged="onHtmlChanged"
    />
  </div>
</template>

<script setup>
import { ref, onMounted, nextTick } from 'vue';
import { MdPreview } from "md-editor-v3";
import "md-editor-v3/lib/preview.css";

const content = defineModel("content", {
  type: String,
  required: true,
});

const anchorIdList = ref([]);
const anchors = ref([]);

// 为每个标题生成唯一ID
const mdHeadingId = (text, level, index) => {
  const anchorId = `${index}`;
  anchorIdList.value.push(anchorId);
  return anchorId;
};

// 初始化标题信息
const mdInit = () => {
  anchors.value = [];
  const hList = anchorIdList.value.map((id) => document.getElementById(id));
  anchors.value = Array.from(hList).map((el, index) => ({
    id: anchorIdList.value[index],
    title: el.innerText,
    active: false,
  }));
};

// 建立IntersectionObserver监听
const observerHList = () => {
  const hList = document.body.querySelectorAll("h1, h2, h3, h4, h5, h6");
  
  const myObserver = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        const { id } = entry.target;
        if (entry.isIntersecting) {
          anchors.value.forEach((anchor) => {
            anchor.active = false;
          });
          const activeAnchor = anchors.value.find((item) => item.id === id);
          if (activeAnchor) {
            activeAnchor.active = true;
          }
        }
      });
    },
    {
      rootMargin: "0px 0px -99% 0px", // 调整触发阈值
    }
  );

  hList.forEach((el) => {
    myObserver.observe(el);
  });
};

// HTML变化后的回调
const onHtmlChanged = async () => {
  await nextTick(); // 确保DOM更新完成
  observerHList();
};

onMounted(() => {
  mdInit();
});

defineExpose({
  anchors,
});
</script>

关键点说明

  1. rootMargin配置:通过设置rootMargin: "0px 0px -99% 0px",我们调整了触发交叉观察的阈值,使得标题元素在进入视口一定比例时才触发回调,这可以避免过于频繁的触发。

  2. nextTick的使用:在onHtmlChanged回调中使用nextTick()确保在DOM更新完成后再建立监听,这是Vue响应式系统的常见模式。

  3. 错误处理:在查找activeAnchor时添加了条件判断,避免在极端情况下找不到对应锚点时出现错误。

扩展思考

这种基于IntersectionObserver的实现方式不仅适用于md-editor-v3,也可以应用于其他需要实现目录导航功能的场景。其优势在于:

  1. 性能高效:基于浏览器原生API实现,性能优于传统的scroll事件监听
  2. 配置灵活:可以通过rootMargin等参数精细控制触发条件
  3. 响应式设计:自动适应内容变化,无需手动维护状态

通过理解md-editor-v3的渲染机制和IntersectionObserver的工作原理,开发者可以更好地处理类似的技术挑战,构建更稳定、更高效的Markdown编辑器功能。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值