干巴爹兔的博客 干巴爹兔的博客
首页
  • 前端文章

    • JavaScript
    • HTML
    • Vue
  • 学习笔记

    • JavaScript教程
    • React学习笔记
    • Electron学习笔记
  • 开源项目

    • cloud-app-admin
    • 下班了吗Vscode插件
    • Subversion变更单插件
  • Server

    • Django
  • 学习笔记

    • MySQL学习笔记
  • 运维

    • 服务器部署
    • Linux
  • 日常学习

    • 学习方法
关于
收藏
友链
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

干巴爹兔

卑微的前端打工人
首页
  • 前端文章

    • JavaScript
    • HTML
    • Vue
  • 学习笔记

    • JavaScript教程
    • React学习笔记
    • Electron学习笔记
  • 开源项目

    • cloud-app-admin
    • 下班了吗Vscode插件
    • Subversion变更单插件
  • Server

    • Django
  • 学习笔记

    • MySQL学习笔记
  • 运维

    • 服务器部署
    • Linux
  • 日常学习

    • 学习方法
关于
收藏
友链
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JavaScript文章

  • 学习笔记

  • 开源项目

  • HTML

  • Vue

    • 分享一个用于Vue的markdown插件
    • Vue分享QQ却只能访问首页的解决方法
    • 自己封装的一个Vue全局Toast插件
    • 使用Vue3.0 beta4与Vite0.6.0制作一个todoList
    • Vue首屏加载提升
    • Vue生成markdown目录索引
      • Vite简单上手
    • 前端
    • Vue
    干巴爹兔
    2020-05-19
    目录

    Vue生成markdown目录索引

    # 开头

    一直以来我的博客的文章内容页都缺少一个目录索引,由于我的文章使用的文本结构为markdown,而现有的插件无法满足我的需求,所以我只能通过过滤文章提取标题动态生成目录结构,同时通过获取各个标题的据顶部高度来实现点击跳转的功能。本教程的代码实现参考了vue使用marked.js实现markdown转html并提取标题生成目录 (opens new window)这篇文章,同时由于我使用Vue结合Vuetify的UI库,部分实现略有不同。

    # 预期结果

    image-20200518102039632

    这个是我的最终实现效果

    # 代码实现

    # html部分

    <template>
      <div class="mx-5">
        <v-container grid-list-xl>
          <v-row>
            <v-col cols="12" md="3" class="link">
              <v-card class="mx-auto mt-2 link_cover">
                <div class="py-4 links">
                  <h3 class="pl-3 pb-3">目录</h3>
                  <ul>
                    <li
                      v-for="(nav, index) in navList"
                      :key="index"
                      :class="{ on: activeIndex === index }"
                      @click="currentClick(index)"
                    >
                      <a href="javascript:;" @click="pageJump(nav.index)">{{
                        nav.title
                      }}</a>
                      <div
                        v-if="nav.children.length > 0"
                        class="menu-children-list"
                      >
                        <ul class="nav-list">
                          <li
                            v-for="(item, idx) in nav.children"
                            :key="idx"
                            :class="{ on: childrenActiveIndex === idx }"
                            @click.stop="childrenCurrentClick(idx)"
                          >
                            <a href="javascript:;" @click="pageJump(item.index)">{{
                              item.title
                            }}</a>
                          </li>
                        </ul>
                      </div>
                    </li>
                  </ul>
                </div>
              </v-card>
            </v-col>
            <v-col cols="12" md="9">
              <div class="body">
                <div
                  class="content markdown-body"
                  ref="helpDocs"
                  v-html="compiledMarkdown"
                ></div>
              </div>
            </v-col>
          </v-row>
        </v-container>
      </div>
    </template>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53

    html部分我使用了Vuetify的UI库的row组件,将目录与文章内容分割开来。

    # JS部分

    <script>
    import marked from "marked";
    
    let rendererMD = new marked.Renderer();
    marked.setOptions({
      renderer: rendererMD,
      gfm: true,
      tables: true,
      breaks: false,
      pedantic: false,
      sanitize: false,
      smartLists: true,
      smartypants: false,
    });
    export default {
      props: ["id"],
      data() {
        return {
          article: [],
          html: "",//文章内容
          navList: [],
          activeIndex: 0,
          docsFirstLevels: [],
          docsSecondLevels: [],
          childrenActiveIndex: 0,
        };
      },
      mounted() {
        this.getArticleDetail();
      },
      methods: {
        async getArticleDetail() {
          try {
            if (this.id) {
              const res = await this.$http.get(`/article?id=${this.id}`);
              this.article = res.data;
              this.html = this.article.html;
              global.console.log(this.article);
              document.getElementsByTagName(
                "title"
              )[0].innerText = this.article.title;
            }
          } catch (e) {
            global.console.log("文章获取异常");
          }
          //文章内容获取后渲染目录,避免目录无法及时获取内容
          this.navList = this.handleNavTree();
          this.getDocsFirstLevels(0);
        },
        childrenCurrentClick(index) {
          this.childrenActiveIndex = index;
        },
        getDocsFirstLevels(times) {
          // 解决图片加载会影响高度问题
          setTimeout(() => {
            let firstLevels = [];
            Array.from(document.querySelectorAll("h3"), (element) => {
              firstLevels.push(element.offsetTop - 60);
            });
            this.docsFirstLevels = firstLevels;
    
            if (times < 8) {
              this.getDocsFirstLevels(times + 1);
            }
          }, 500);
        },
        getDocsSecondLevels(parentActiveIndex) {
          let idx = parentActiveIndex;
          let secondLevels = [];
          let navChildren = this.navList[idx].children;
    
          if (navChildren.length > 0) {
            secondLevels = navChildren.map((item) => {
              return this.$el.querySelector(`#data-${item.index}`).offsetTop - 60;
            });
            this.docsSecondLevels = secondLevels;
          }
        },
        getLevelActiveIndex(scrollTop, docsLevels) {
          let currentIdx = null;
          let nowActive = docsLevels.some((currentValue, index) => {
            if (currentValue >= scrollTop) {
              currentIdx = index;
              return true;
            }
          });
    
          currentIdx = currentIdx - 1;
    
          if (nowActive && currentIdx === -1) {
            currentIdx = 0;
          } else if (!nowActive && currentIdx === -1) {
            currentIdx = docsLevels.length - 1;
          }
          return currentIdx;
        },
        pageJump(id) {
          this.titleClickScroll = true;
          //这里我与原作者的不太一样,发现原作者的scrollTop一直为0,所以使用了Vuetify自带的goTo事件
           this.$vuetify.goTo(this.$el.querySelector(`#data-${id}`).offsetTop - 40);
          setTimeout(() => (this.titleClickScroll = false), 100);
        },
        currentClick(index) {
          this.activeIndex = index;
          this.getDocsSecondLevels(index);
        },
        getTitle(content) {
          let nav = [];
    
          let tempArr = [];
          content.replace(/(#+)[^#][^\n]*?(?:\n)/g, function(match, m1) {
            let title = match.replace("\n", "");
            let level = m1.length;
            tempArr.push({
              title: title.replace(/^#+/, "").replace(/\([^)]*?\)/, ""),
              level: level,
              children: [],
            });
          });
    
          // 只处理二级到四级标题,以及添加与id对应的index值,这里还是有点bug,因为某些code里面的注释可能有多个井号
          nav = tempArr.filter((item) => item.level >= 2 && item.level <= 4);
          global.console.log(nav);
          let index = 0;
          return (nav = nav.map((item) => {
            item.index = index++;
            return item;
          }));
        },
        // 将一级二级标题数据处理成树结构
        handleNavTree() {
          let navs = this.getTitle(this.content);
          let navLevel = [3, 4];
          let retNavs = [];
          let toAppendNavList;
    
          navLevel.forEach((level) => {
            // 遍历一级二级标题,将同一级的标题组成新数组
            toAppendNavList = this.find(navs, {
              level: level,
            });
    
            if (retNavs.length === 0) {
              // 处理一级标题
              retNavs = retNavs.concat(toAppendNavList);
            } else {
              // 处理二级标题,并将二级标题添加到对应的父级标题的children中
              toAppendNavList.forEach((item) => {
                item = Object.assign(item);
                let parentNavIndex = this.getParentIndex(navs, item.index);
                return this.appendToParentNav(retNavs, parentNavIndex, item);
              });
            }
          });
          return retNavs;
        },
        find(arr, condition) {
          return arr.filter((item) => {
            for (let key in condition) {
              if (condition.hasOwnProperty(key) && condition[key] !== item[key]) {
                return false;
              }
            }
            return true;
          });
        },
        getParentIndex(nav, endIndex) {
          for (var i = endIndex - 1; i >= 0; i--) {
            if (nav[endIndex].level > nav[i].level) {
              return nav[i].index;
            }
          }
        },
        appendToParentNav(nav, parentIndex, newNav) {
          let index = this.findIndex(nav, {
            index: parentIndex,
          });
          nav[index].children = nav[index].children.concat(newNav);
        },
        findIndex(arr, condition) {
          let ret = -1;
          arr.forEach((item, index) => {
            for (var key in condition) {
              if (condition.hasOwnProperty(key) && condition[key] !== item[key]) {
                return false;
              }
            }
            ret = index;
          });
          return ret;
        },
      },
      computed: {
        content() {
          return this.html;
        },
        //此函数将markdown内容进一步的转换
        compiledMarkdown: function() {
          let index = 0;
          rendererMD.heading = function(text, level) {
            //我比较习惯三级和四级目录,这里看你喜欢
            if (level <= 4) {
              return `<h${level} id="data-${index++}">${text}</h${level}>`;
            } else {
              return `<h${level}>${text}</h${level}>`;
            }
          };
    
          return marked(this.content);
        },
      },
    };
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213

    js部分需要安装marked这个库npm i -s marked,我参考了原作者的实现,大致就是利用marked先将markdown内容转化为标题带有id为data-${index++}的内容,后续提供数组的操作形成一个目录结构,具体实现可以自己研究一下。

    # Css部分

    <style scoped>
    .content {
      padding: 8px 8px;
      font-size: 14px;
    }
    .body {
      margin-top: 24px;
      background: #f0f0f0;
      border-radius: 5px;
    }
    ul {
      list-style-type: none;
      padding: 2px 6px;
    }
    li {
      list-style-type: none;
      margin: 2px 6px;
    }
    a {
      color: #42b983;
      text-decoration: none;
    }
    @media screen and (min-width: 960px) {
      .link {
        padding-top: 100px;
        position: fixed;
        right: 25px;
        top: 100;
      }
      .link_cover {
        max-height: 400px;
        overflow: scroll;
        overflow-x: hidden;
        overflow-y: visible;
      }
    }
    @media screen and (min-width: 1060px) {
      .link {
        padding-top: 100px;
        position: fixed;
        right: 50px;
        top: 100;
      }
      .link_cover {
        max-height: 400px;
        overflow: scroll;
        overflow-x: hidden;
        overflow-y: visible;
      }
    }
    </style>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51

    css部分我将默认的ul和li做了美化,同时使用了媒体查询,在大尺寸设备上我希望能够将目录固定,便于文章的浏览,同时希望目录的最大高度不要太高,以免目录太复杂导致超出文章高度无法查看。最终实现效果如下:

    image-20200518103325059

    编辑 (opens new window)
    #Vue
    上次更新: 2022/08/29, 16:40:19
    Vue首屏加载提升
    Vite简单上手

    ← Vue首屏加载提升 Vite简单上手→

    最近更新
    01
    使用Vscode开发一个小插件
    10-21
    02
    Vscode插件配置项监听
    10-18
    03
    使用has属性构造必填效果
    10-14
    更多文章>
    Theme by Vdoing | Copyright © 2020-2023 互联网ICP备案: 闽ICP备18027236号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式