先上效果图

如图所示,在网页中,能直接解析markdown文档,并且生成目录大纲,也支持点击目录标题跳转到对应栏目中,下面就来讲讲是如何实现此功能的。
1、下载vue-markdown
yarn add vue-markdown
2、在页面中渲染markdown文件
<template>
     <div>
        <VueMarkdown
            class="api-content"
            :source="markdownContent"
            id="content"
        />
    </div>
</template>
<script>
import VueMarkdown from "vue-markdown";
export default {
    components: {
        VueMarkdown,
    },
    data() {
        return {
            markdownContent: "",
         };
    },
    mounted() {
        this.loadMarkdownFile();
    },
    methods: {
        async loadMarkdownFile() {
            try {
                // api.md文件存放在根目录下的public文件夹中
                const response = await fetch("/api.md");
                const markdownText = await response.text();
                this.markdownContent = markdownText;
 
            } catch (error) {
                console.error("Failed to load the Markdown file:", error);
            }
        }, 
    },
};
</script> 
此时,打开浏览器查看,页面中已经正常渲染markdown文件了。
3、生成目录大纲
现在,我们需要有目录大纲方便我们查看文档。我的思路是,首先拿到markdown文件的html结构,然后找到所有H1-H5的标题标签,并给这些标签设置id,生成一个新数组,通过这个数组生成目录结构,说干就干。
//html部分
<div class="api-tree" id="tree">
    <el-tree
        :data="tree"
        :default-expand-all="true"
        @node-click="handleNodeClick"
    ></el-tree>
</div>
// js部分
catalogTree() {
    const content = document.getElementById("content").children;
    var arr = [];
    let currentHightestLevel;
    let parentId;
    let index = 0;
    for (let i = 0; i < content.length; i++) {
        let header = content[i].localName;
        if (/\b[h][0-9]\b/.test(header)) {
            let ele = $(content[i]);
            let name = ele.text();
            // 设置id
            ele.attr("id", i);
            let id = i;
            if (index === 0 || header <= currentHightestLevel) {
                currentHightestLevel = header;
                parentId = id;
            }
            arr.push({
                id: id,
                label: name,
                parentId: parentId == id ? "0" : parentId,
            });
            index++;
        }
    }
    const tree = [];
    arr.forEach((item) => {
        if (item.parentId === "0") {
            tree.push(this.convertArrayToTree(arr, item));
        }
    });
    this.tree = tree;
},
convertArrayToTree(arr, node) {
    for (let i = 0; i < arr.length; i++) {
        if (arr[i].parentId === node.id) {
            const res = this.convertArrayToTree(arr, arr[i]);
            if (node.children) {
                node.children.push(res);
            } else {
                node.children = [res];
            }
        }
    }
    return node;
},
handleNodeClick(data) {
    let anchorElement = document.getElementById(data.id);
    if (anchorElement) {
         anchorElement.scrollIntoView({
             behavior: "smooth",
             block: "end",
         });
     }
}, 
4、大功告成,最后,附上全部代码,带css样式
<template>
    <div class="page-api" id="myElement">
        <div class="api-tree" id="tree">
            <el-tree
                :data="tree"
                :default-expand-all="true"
                @node-click="handleNodeClick"
            ></el-tree>
        </div>
        <VueMarkdown
            class="api-content"
            :source="markdownContent"
            id="content"
        />
    </div>
</template>
<script>
import VueMarkdown from "vue-markdown";
import $ from "jquery";
export default {
    components: {
        VueMarkdown,
    },
    data() {
        return {
            markdownContent: "",
            tree: [],
        };
    },
    mounted() {
        this.loadMarkdownFile();
    },
    methods: {
        async loadMarkdownFile() {
            try {
                const response = await fetch("/api.md");
                const markdownText = await response.text();
                this.markdownContent = markdownText;
                this.$nextTick(() => {
                    this.catalogTree();
                });
            } catch (error) {
                console.error("Failed to load the Markdown file:", error);
            }
        },
        catalogTree() {
            const content = document.getElementById("content").children;
            var arr = [];
            let currentHightestLevel;
            let parentId;
            let index = 0;
            for (let i = 0; i < content.length; i++) {
                let header = content[i].localName;
                if (/\b[h][0-9]\b/.test(header)) {
                    let ele = $(content[i]);
                    let name = ele.text();
                    // 设置id
                    ele.attr("id", i);
                    // let id = ele.children("a").attr("id");
                    let id = i;
                    if (index === 0 || header <= currentHightestLevel) {
                        currentHightestLevel = header;
                        parentId = id;
                    }
                    arr.push({
                        id: id,
                        label: name,
                        parentId: parentId == id ? "0" : parentId,
                    });
                    index++;
                }
            }
            const tree = [];
            arr.forEach((item) => {
                if (item.parentId === "0") {
                    tree.push(this.convertArrayToTree(arr, item));
                }
            });
            this.tree = tree;
        },
        convertArrayToTree(arr, node) {
            for (let i = 0; i < arr.length; i++) {
                if (arr[i].parentId === node.id) {
                    const res = this.convertArrayToTree(arr, arr[i]);
                    if (node.children) {
                        node.children.push(res);
                    } else {
                        node.children = [res];
                    }
                }
            }
            return node;
        },
        handleNodeClick(data) {
            let anchorElement = document.getElementById(data.id);
            let scrollPosition = anchorElement.offsetTop - 20;
            let myElement = document.getElementById("myElement");
            myElement.scrollTo({
                left: 0,
                top: scrollPosition,
                behavior: "smooth",
            });
            // if (anchorElement) {
            //     anchorElement.scrollIntoView({
            //         behavior: "smooth",
            //         block: "end",
            //     });
            // }
        },
    },
};
</script>
<style lang="scss">
.page-api {
    display: flex;
    height: 100%;
    overflow-y: scroll;
    .api-tree {
        position: fixed;
        left: 20px;
        top: 120px;
        width: 200px;
        height: calc(100% - 160px);
        overflow-y: scroll;
        z-index: 99;
        .el-tree {
            background: none;
            color: #fff;
            .el-tree-node:focus > .el-tree-node__content,
            .el-tree-node__content:hover {
                background: none;
                color: rgb(24, 144, 255);
            }
        }
    }
    .api-content {
        flex: 1;
        margin-left: 220px;
        padding: 0 30px;
        color: rgba(255, 255, 255, 0.75);
        h3 {
            margin-left: 25px;
        }
        code {
            border-radius: 2px;
            color: #e96900;
            margin: 0 2px;
            padding: 3px 5px;
            white-space: pre-wrap;
        }
        table {
            border-collapse: collapse;
            border-spacing: 0;
            th,
            td {
                border: 1px solid #ddd;
                padding: 6px 13px;
                margin: 0;
            }
        }
        pre {
            background: rgba(0, 0, 0, 0.7);
            padding: 20px 30px;
        }
    }
}
</style>
 
                

















