找回密码
 立即注册
首页 业界区 业界 给Markdown渲染网页增加一个目录组件(Vite+Vditor+Handl ...

给Markdown渲染网页增加一个目录组件(Vite+Vditor+Handlebars)(上)

祖柔惠 2025-6-12 19:06:24
1 引言

在上一篇文章《解决Vditor加载Markdown网页很慢的问题(Vite+JS+Vditor)》中,我们通过设置域内CDN的方式解决Vditor加载Markdown网页很慢的问题。而在这篇文章中,笔者将会开发实现一个前端中很常见的需求:给基于Markdown渲染的文档网页增加一个目录组件。
需要说明的是,原生的Markdown标准并没有规定生成目录的写法,但是国内的博文网站似乎都支持一个拓展来实现目录的生成:
  1. [toc]
复制代码
但是这样生成的目录是通常是位于文章页面的最上方,这样就失去了目录的意义。比较好的实现是像CSDN或者掘金一样,额外生成一个目录组件,并且固定在侧栏上方。这样可以在浏览文章的时候,随时定位所在的目录;同时还可以使用目录来导航。
1.png

阅读本文可能需要的前置文章:

  • 《通过JS模板引擎实现动态模块组件(Vite+JS+Handlebars)》
  • 《使用Vditor将Markdown文档渲染成网页(Vite+JS+Vditor)》
2 详叙

2.1 整体结构

将渲染Markdown文档的部分封装成单独的组件(post-article.js、post-article.handlebars和post-article.css),增加一个文章目录组件(post-toc.js、post-toc.handlebars、post-toc.css)。另外post-data.json是我们提前准备的博客文章,里面除了保存有Markdown格式的文档字符串,还有一些文章的相关数据;1.png和2.png则是文章中图片。项目组织结构如下:
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
还是按照代码的执行顺序来介绍这个功能的实现。首先还是index.html:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="UTF-8" />
  5.   <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  6.   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7.   <title>Vite App</title>
  8. </head>
  9. <body>
  10.   
  11.    
  12.    
  13.   
  14.   
  15. </body>
  16. </html>
复制代码
主要就是增加了post-article-placeholder和article-toc-placeholder这两个元素,分别作为Markdown博文和博文目录的容器。其实这里面还有个页面布局的问题,不过这个问题我们下一篇文章再说。这里还是先看main.js:
  1. import "./style.css";
  2. import "./components/post-article.js";
复制代码
2.2 博文内容组件

引用了post-article.js,也就是Markdown博文内容组件。那么就进入post-article.js:
  1. import "./post-article.css";
  2. import { CreateTocPanel } from "./post-toc.js";
  3. import Handlebars from "handlebars";
  4. import templateSource from "./post-article.handlebars?raw";
  5. import "vditor/dist/index.css";
  6. import Vditor from "vditor";
  7. // 初始化文章标签面板
  8. async function InitializePostArticlePanel() {
  9.   try {   
  10.     const response = await fetch("/post-data.json");
  11.     if (!response.ok) {
  12.       throw new Error("网络无响应");
  13.     }
  14.     const blogData = await response.json();
  15.   
  16.     // 编译模板
  17.     const template = Handlebars.compile(templateSource);
  18.     // 渲染模板
  19.     const renderedHtml = template({
  20.       blogMeta: blogData.blogMeta,
  21.     });
  22.     // 将渲染好的HTML插入到页面中
  23.     document.getElementById("post-article-placeholder").innerHTML =
  24.       renderedHtml;
  25.     // 显示内容
  26.     Vditor.preview(document.getElementById("post-content"), blogData.content, {
  27.       cdn: window.location.origin,
  28.       markdown: {
  29.         toc: false,
  30.         mark: true, //==高亮显示==
  31.         footnotes: true, //脚注
  32.         autoSpace: true, //自动空格,适合中英文混合排版
  33.       },
  34.       math: {
  35.         engine: "KaTeX", //支持latex公式
  36.         inlineDigit: true, //内联公式可以接数字
  37.       },
  38.       hljs: {
  39.         style: "github", //代码段样式
  40.         lineNumber: true, //是否显示行号
  41.       },
  42.       anchor: 2, // 为标题添加锚点 0:不渲染;1:渲染于标题前;2:渲染于标题后
  43.       lang: "zh_CN", //中文
  44.       theme: {
  45.         current: "light", //light,dark,light,wechat
  46.       },
  47.       lazyLoadImage:
  48.         "https://cdn.jsdelivr.net/npm/vditor/dist/images/img-loading.svg",
  49.       transform: (html) => {
  50.         // 使用正则表达式替换图片路径,并添加居中样式及题注
  51.         return html.replace(
  52.           /<img\s+[^>]*src="\.\/([^"]+)\.([a-zA-Z0-9]+)"\s*alt="([^"]*)"[^>]*>/g,
  53.           (match, p1, p2, altText) => {
  54.             // const newSrc = `${backendUrl}/blogs/resources/images/${postId}/${p1}.${p2}`;
  55.             const newSrc = `${p1}.${p2}`;
  56.             const imgWithCaption = `
  57.                     
  58.                         <img src="https://www.cnblogs.com/${newSrc}"  alt="${altText}">
  59.                         <p >${altText}</p>
  60.                     
  61.                     `;
  62.             return imgWithCaption;
  63.           }
  64.         );
  65.       },
  66.       after() {
  67.         CreateTocPanel();
  68.       },
  69.     });
  70.   } catch (error) {
  71.     console.error("获取博客失败:", error);
  72.   }
  73. }
  74. document.addEventListener("DOMContentLoaded", InitializePostArticlePanel);
复制代码
post-article.js中的内容改进自《通过JS模板引擎实现动态模块组件(Vite+JS+Handlebars)》中的案例,不过略有不同。首先是获取博文数据:
  1. const response = await fetch("/post-data.json");
  2. if (!response.ok) {
  3.     throw new Error("网络无响应");
  4. }
  5. const blogData = await response.json();
  6. // 编译模板
  7. const template = Handlebars.compile(templateSource);
  8. // 渲染模板
  9. const renderedHtml = template({
  10.     blogMeta: blogData.blogMeta,
  11. });
  12. // 将渲染好的HTML插入到页面中
  13. document.getElementById("post-article-placeholder").innerHTML =
  14.     renderedHtml;
复制代码
在实际项目开发中,应该是从远端API获取数据,这里进行了简化,将数据提前准备好了放置在域内。然后,将这个数据与编译的Handlebars模板一起渲染成HTML元素。从下面的post-article.handlebars中可以看到,博文组件中内容不仅包含Markdown博文内容元素,还有诸如时间、统计信息、标签等元素:
[code]    {{blogMeta.title}}

            <span class = "post-stat">            
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册