<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      給Markdown渲染網(wǎng)頁(yè)增加一個(gè)目錄組件(Vite+Vditor+Handlebars)(上)

      1 引言

      在上一篇文章《解決Vditor加載Markdown網(wǎng)頁(yè)很慢的問(wèn)題(Vite+JS+Vditor)》中,我們通過(guò)設(shè)置域內(nèi)CDN的方式解決Vditor加載Markdown網(wǎng)頁(yè)很慢的問(wèn)題。而在這篇文章中,筆者將會(huì)開(kāi)發(fā)實(shí)現(xiàn)一個(gè)前端中很常見(jiàn)的需求:給基于Markdown渲染的文檔網(wǎng)頁(yè)增加一個(gè)目錄組件。

      需要說(shuō)明的是,原生的Markdown標(biāo)準(zhǔn)并沒(méi)有規(guī)定生成目錄的寫(xiě)法,但是國(guó)內(nèi)的博文網(wǎng)站似乎都支持一個(gè)拓展來(lái)實(shí)現(xiàn)目錄的生成:

      [toc]
      

      但是這樣生成的目錄是通常是位于文章頁(yè)面的最上方,這樣就失去了目錄的意義。比較好的實(shí)現(xiàn)是像CSDN或者掘金一樣,額外生成一個(gè)目錄組件,并且固定在側(cè)欄上方。這樣可以在瀏覽文章的時(shí)候,隨時(shí)定位所在的目錄;同時(shí)還可以使用目錄來(lái)導(dǎo)航。

      掘金博文側(cè)欄的目錄組件,固定在網(wǎng)頁(yè)右上方

      閱讀本文可能需要的前置文章:

      2 詳敘

      2.1 整體結(jié)構(gòu)

      將渲染Markdown文檔的部分封裝成單獨(dú)的組件(post-article.js、post-article.handlebars和post-article.css),增加一個(gè)文章目錄組件(post-toc.js、post-toc.handlebars、post-toc.css)。另外post-data.json是我們提前準(zhǔn)備的博客文章,里面除了保存有Markdown格式的文檔字符串,還有一些文章的相關(guān)數(shù)據(jù);1.png和2.png則是文章中圖片。項(xiàng)目組織結(jié)構(gòu)如下:

      my-native-js-app/
      ├── public/
      │ ├── 1.png
      │ ├── 2.png
      │ └── post-data.json
      ├── src/
      │ ├── components/
      │ │ ├── post-article.css
      │ │ ├── post-article.handlebars
      │ │ ├── post-article.js
      │ │ ├── post-toc.css
      │ │ ├── post-toc.handlebars
      │ │ └── post-toc.js
      │ ├── main.js
      │ └── style.css
      ├── index.html
      └── package.json

      還是按照代碼的執(zhí)行順序來(lái)介紹這個(gè)功能的實(shí)現(xiàn)。首先還是index.html:

      <!DOCTYPE html>
      <html lang="en">
      
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/vite.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Vite App</title>
      </head>
      
      <body>
        <div id="app">
          <div id="post-article-placeholder"></div>
          <div id="article-toc-placeholder"></div>
        </div>
        <script type="module" src="/src/main.js"></script>
      </body>
      
      </html>
      

      主要就是增加了post-article-placeholder和article-toc-placeholder這兩個(gè)元素,分別作為Markdown博文和博文目錄的容器。其實(shí)這里面還有個(gè)頁(yè)面布局的問(wèn)題,不過(guò)這個(gè)問(wèn)題我們下一篇文章再說(shuō)。這里還是先看main.js:

      import "./style.css";
      import "./components/post-article.js";
      

      2.2 博文內(nèi)容組件

      引用了post-article.js,也就是Markdown博文內(nèi)容組件。那么就進(jìn)入post-article.js:

      import "./post-article.css";
      import { CreateTocPanel } from "./post-toc.js";
      import Handlebars from "handlebars";
      import templateSource from "./post-article.handlebars?raw";
      
      import "vditor/dist/index.css";
      import Vditor from "vditor";
      
      // 初始化文章標(biāo)簽面板
      async function InitializePostArticlePanel() {
        try {   
          const response = await fetch("/post-data.json");
          if (!response.ok) {
            throw new Error("網(wǎng)絡(luò)無(wú)響應(yīng)");
          }
          const blogData = await response.json();
        
          // 編譯模板
          const template = Handlebars.compile(templateSource);
      
          // 渲染模板
          const renderedHtml = template({
            blogMeta: blogData.blogMeta,
          });
      
          // 將渲染好的HTML插入到頁(yè)面中
          document.getElementById("post-article-placeholder").innerHTML =
            renderedHtml;
      
          // 顯示內(nèi)容
          Vditor.preview(document.getElementById("post-content"), blogData.content, {
            cdn: window.location.origin,
            markdown: {
              toc: false,
              mark: true, //==高亮顯示==
              footnotes: true, //腳注
              autoSpace: true, //自動(dòng)空格,適合中英文混合排版
            },
            math: {
              engine: "KaTeX", //支持latex公式
              inlineDigit: true, //內(nèi)聯(lián)公式可以接數(shù)字
            },
            hljs: {
              style: "github", //代碼段樣式
              lineNumber: true, //是否顯示行號(hào)
            },
            anchor: 2, // 為標(biāo)題添加錨點(diǎn) 0:不渲染;1:渲染于標(biāo)題前;2:渲染于標(biāo)題后
            lang: "zh_CN", //中文
            theme: {
              current: "light", //light,dark,light,wechat
            },
            lazyLoadImage:
              "https://cdn.jsdelivr.net/npm/vditor/dist/images/img-loading.svg",
            transform: (html) => {
              // 使用正則表達(dá)式替換圖片路徑,并添加居中樣式及題注
              return html.replace(
                /<img\s+[^>]*src="\.\/([^"]+)\.([a-zA-Z0-9]+)"\s*alt="([^"]*)"[^>]*>/g,
                (match, p1, p2, altText) => {
                  // const newSrc = `${backendUrl}/blogs/resources/images/${postId}/${p1}.${p2}`;
                  const newSrc = `${p1}.${p2}`;
                  const imgWithCaption = `
                          <div style="text-align: center;">
                              <img src="${newSrc}" class="center-image" alt="${altText}">
                              <p class="caption">${altText}</p>
                          </div>
                          `;
                  return imgWithCaption;
                }
              );
            },
            after() {
              CreateTocPanel();
            },
          });
        } catch (error) {
          console.error("獲取博客失敗:", error);
        }
      }
      
      document.addEventListener("DOMContentLoaded", InitializePostArticlePanel);
      

      post-article.js中的內(nèi)容改進(jìn)自《通過(guò)JS模板引擎實(shí)現(xiàn)動(dòng)態(tài)模塊組件(Vite+JS+Handlebars)》中的案例,不過(guò)略有不同。首先是獲取博文數(shù)據(jù):

      const response = await fetch("/post-data.json");
      if (!response.ok) {
          throw new Error("網(wǎng)絡(luò)無(wú)響應(yīng)");
      }
      const blogData = await response.json();
      
      // 編譯模板
      const template = Handlebars.compile(templateSource);
      
      // 渲染模板
      const renderedHtml = template({
          blogMeta: blogData.blogMeta,
      });
      
      // 將渲染好的HTML插入到頁(yè)面中
      document.getElementById("post-article-placeholder").innerHTML =
          renderedHtml;
      

      在實(shí)際項(xiàng)目開(kāi)發(fā)中,應(yīng)該是從遠(yuǎn)端API獲取數(shù)據(jù),這里進(jìn)行了簡(jiǎn)化,將數(shù)據(jù)提前準(zhǔn)備好了放置在域內(nèi)。然后,將這個(gè)數(shù)據(jù)與編譯的Handlebars模板一起渲染成HTML元素。從下面的post-article.handlebars中可以看到,博文組件中內(nèi)容不僅包含Markdown博文內(nèi)容元素,還有諸如時(shí)間、統(tǒng)計(jì)信息、標(biāo)簽等元素:

      <div id="main-content">
          <h1 id="post-title">{{blogMeta.title}}</h1>
          <div class="post-stats">
              <span class = "post-stat">
                  <span>??</span><span class = "text">已于</span>{{blogMeta.createdTime}}<span class = "text">修改</span>
              </span>
              <span class = "post-stat">
                  <span>???</span>{{blogMeta.postStats.viewCount}}<span class = "text">閱讀</span>
              </span>
              <span class = "post-stat">
                  <span>??</span>{{blogMeta.postStats.likeCount}}<span class = "text">點(diǎn)贊</span>
              </span>
              <span class = "post-stat">
                  <span>??</span>{{blogMeta.postStats.commentCount}}<span class = "text">評(píng)論</span>
              </span>
          </div>
          <div class="post-tags">
              <span class = "tags-title">
                  <span>??</span><span class = "text">文章標(biāo)簽</span>
              </span>
              {{#each blogMeta.tagNames}}
              <span class = "post-tag">{{this}}</span>
              {{/each}}
          </div>
          <div class="post-categories">
              專(zhuān)欄
              {{#each blogMeta.categoryNames}}
              <span> {{this}} </span>
              {{/each}}
              收錄該內(nèi)容
          </div>
          <div id="post-content"></div>
      </div>
      

      Markdown博文內(nèi)容元素是使用Vditor來(lái)渲染初始化的,這一點(diǎn)與之前的案例一樣。不同的是增加了一個(gè)after配置:

      import { CreateTocPanel } from "./post-toc.js";
      
      //...
      
      after() {
          CreateTocPanel();
      },
      

      這個(gè)after配置的意思是當(dāng)Vditor渲染完成以后,就立刻執(zhí)行CreateTocPanel()函數(shù),這個(gè)函數(shù)來(lái)自于博文目錄組件post-toc.js,表示要開(kāi)始創(chuàng)建博文目錄了。

      2.2 博文目錄組件

      post-toc.js中的代碼如下所示:

      import "./post-toc.css";
      
      import Handlebars from "handlebars";
      import templateSource from "./post-toc.handlebars?raw";
      
      export function CreateTocPanel() {
        const headings = document.querySelectorAll(
          "#post-content h1, #post-content h2, #post-content h3"
        );
      
        const tocContent = [];
        headings.forEach((heading, index) => {
          const content = {};
          content["id"] = heading.id;
          content["title"] = heading.textContent;
          const marginLeft =
            heading.tagName === "H2" ? 20 : heading.tagName === "H3" ? 40 : 0;
          content["marginLeft"] = marginLeft;
          tocContent.push(content);
        });
      
        // 編譯模板
        const template = Handlebars.compile(templateSource);
      
        // 渲染模板
        const renderedHtml = template({
          tocContent,
        });
      
        // 將渲染好的HTML插入到頁(yè)面中
        const articleTocPlaceholder = document.getElementById(
          "article-toc-placeholder"
        );
        articleTocPlaceholder.innerHTML = renderedHtml;
      
        // 聯(lián)動(dòng):滾動(dòng)時(shí)同步激活目錄項(xiàng)
        window.addEventListener("scroll", () => {
          let activeHeading;
          headings.forEach((heading) => {
            const rect = heading.getBoundingClientRect();
            if (rect.top >= 0 && rect.top <= window.innerHeight / 2) {
              activeHeading = heading;
            }
          });
      
          if (activeHeading) {
            document
              .querySelectorAll(".toc-sidebar .toc a")
              .forEach((link) => link.classList.remove("active"));     
            const escapedId = CSS.escape(activeHeading.id); //安全地轉(zhuǎn)義選擇器中的特殊字符
            const activeLink = document.querySelector(
              `.toc-sidebar .toc a[href="#${escapedId}"]`
            );
            if (activeLink) activeLink.classList.add("active");
          }
        });
      }
      

      這段代碼是實(shí)現(xiàn)博文目錄功能的關(guān)鍵代碼。首先,搜索查詢(xún)渲染成HTML形式的博文內(nèi)容中的標(biāo)題元素h1h2h3

      const headings = document.querySelectorAll(
          "#post-content h1, #post-content h2, #post-content h3"
        );
      

      然后提取出關(guān)鍵數(shù)據(jù):

      const tocContent = [];
        headings.forEach((heading, index) => {
          const content = {};
          content["id"] = heading.id;
          content["title"] = heading.textContent;
          const marginLeft =
            heading.tagName === "H2" ? 20 : heading.tagName === "H3" ? 40 : 0;
          content["marginLeft"] = marginLeft;
          tocContent.push(content);
        });
      

      將其傳入Handlebars模板進(jìn)行渲染:

      // 編譯模板
        const template = Handlebars.compile(templateSource);
      
        // 渲染模板
        const renderedHtml = template({
          tocContent,
        });
      
        // 將渲染好的HTML插入到頁(yè)面中
        const articleTocPlaceholder = document.getElementById(
          "article-toc-placeholder"
        );
        articleTocPlaceholder.innerHTML = renderedHtml;
      

      模板post-toc.handlebars中的內(nèi)容非常簡(jiǎn)單:

      <div class="toc-sidebar">
          <div class="toc">
              <h3>文章目錄</h3>
              <ul>
                  {{#each tocContent}}
                  <li style="margin-left: {{marginLeft}}px;">
                      <a href="#{{id}}" class="">
                          {{title}}
                      </a>
                  </li>
                  {{/each}}
              </ul>
          </div>
      </div>
      

      可以看到這里能夠獲取一級(jí)、二級(jí)還有三級(jí)標(biāo)題,通過(guò)樣式的縮進(jìn)(margin-left)來(lái)體現(xiàn)標(biāo)題的不同。另外,href屬性的設(shè)置也保證了能通過(guò)點(diǎn)擊來(lái)實(shí)現(xiàn)跳轉(zhuǎn)。

      最后實(shí)現(xiàn)聯(lián)動(dòng),通過(guò)文章標(biāo)題元素范圍的判定,來(lái)高亮目錄中標(biāo)題元素的樣式,讓用戶(hù)直到瀏覽到博文中的哪一段了:

      // 聯(lián)動(dòng):滾動(dòng)時(shí)同步激活目錄項(xiàng)
        window.addEventListener("scroll", () => {
          let activeHeading;
          headings.forEach((heading) => {
            const rect = heading.getBoundingClientRect();
            if (rect.top >= 0 && rect.top <= window.innerHeight / 2) {
              activeHeading = heading;
            }
          });
      
          if (activeHeading) {
            document
              .querySelectorAll(".toc-sidebar .toc a")
              .forEach((link) => link.classList.remove("active"));     
            const escapedId = CSS.escape(activeHeading.id); //安全地轉(zhuǎn)義選擇器中的特殊字符
            const activeLink = document.querySelector(
              `.toc-sidebar .toc a[href="#${escapedId}"]`
            );
            if (activeLink) activeLink.classList.add("active");
          }
        });
      

      3 結(jié)語(yǔ)

      最終實(shí)現(xiàn)的效果如下圖所示:

      博文目錄組件最終效果

      雖然功能大致實(shí)現(xiàn)了,不過(guò)還有一些問(wèn)題沒(méi)有說(shuō)清楚,比如在瀏覽文章的過(guò)程中,博文目錄是如何始終保證黏在頁(yè)面的右上角的?這個(gè)問(wèn)題就放在下篇中繼續(xù)論述了。

      實(shí)現(xiàn)代碼

      posted @ 2025-06-12 20:51  charlee44  閱讀(309)  評(píng)論(0)    收藏  舉報(bào)
      主站蜘蛛池模板: 久久精品国产99久久6| 国产成人啪精品视频免费软件| 色狠狠色婷婷丁香五月| 精品无码人妻一区二区三区| 日产精品99久久久久久| 大兴区| 国产一区二区三区乱码在线观看 | 99久久er热在这里只有精品99| 日本高清成本人视频一区| 成人av午夜在线观看| 在线观看特色大片免费视频| 国产偷人妻精品一区二区在线| 94人妻少妇偷人精品| 博乐市| 实拍女处破www免费看| 99久久无色码中文字幕| 久久国产精品-国产精品| 国产av一区二区久久蜜臀 | 精品久久久无码中文字幕| 国产亚洲欧洲av综合一区二区三区| 狠狠色噜噜狠狠亚洲AV| 欧美日产国产精品日产| 欧洲美熟女乱又伦免费视频| 自拍视频亚洲精品在线| gogogo高清在线播放免费| 久国产精品韩国三级视频| 中文字幕精品人妻av在线| 国产精品黄色精品黄色大片| 香蕉久久一区二区不卡无毒影院| 青草热在线观看精品视频| 一区二区亚洲人妻精品| 好深好湿好硬顶到了好爽| 国产熟妇久久777777| 日本国产精品第一页久久| 粗大猛烈进出高潮视频| 狠狠色噜噜狠狠狠狠2021| 欧洲中文字幕国产精品| jk白丝喷浆| 免费看黄色片| 日韩精品亚洲国产成人av| 韩国精品一区二区三区|