Node.js学习Demo之Markdown 转 HTML (附仓库完整源码)

195 阅读10分钟

仓库源码 gitee.com/jelly-mulbe…

一、介绍

什么是 Markdown ?

Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档。

三个库分别是什么?

EJS : 一款强大的 JavaScript模板 引擎,它可以帮助我们在HTML中嵌入动态内容。使用EJS,您可以轻松地将Markdown转换为美观的HTML页面。

Marked: 一个流行的Markdown解析器和编译器,它可以将Markdown语法转换为 HTML 标记。

BrowserSync: 一个强大的开发工具,它可以帮助您实时预览和同步您的网页更改。当您对Markdown文件进行编辑并将其转换为HTML时,BrowserSync可以自动刷新您的浏览器,使您能够即时查看转换后的结果

二、 实操

  1. 安装三个库
npm i ejs marked browser-sync

2. 新建

JavaScrip 模版引擎

template.js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= title %></title>
    <link rel="stylesheet" href="./index.css">
</head>
<body>
    <%- content %>
</body>
</html>
新建 CSS 样式

细心的小伙伴可以看到上面模版 ejs 中有样式

index.css

/* Markdown通用样式 */

/* 设置全局字体样式 */
body {
    font-family: Arial, sans-serif;
    font-size: 16px;
    line-height: 1.6;
    color: #333;
  }
  
  /* 设置标题样式 */
  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    margin-top: 1.3em;
    margin-bottom: 0.6em;
    font-weight: bold;
  }
  
  h1 {
    font-size: 2.2em;
  }
  
  h2 {
    font-size: 1.8em;
  }
  
  h3 {
    font-size: 1.6em;
  }
  
  h4 {
    font-size: 1.4em;
  }
  
  h5 {
    font-size: 1.2em;
  }
  
  h6 {
    font-size: 1em;
  }
  
  /* 设置段落样式 */
  p {
    margin-bottom: 1.3em;
  }
  
  /* 设置链接样式 */
  a {
    color: #337ab7;
    text-decoration: none;
  }
  
  a:hover {
    text-decoration: underline;
  }
  
  /* 设置列表样式 */
  ul,
  ol {
    margin-top: 0;
    margin-bottom: 1.3em;
    padding-left: 2em;
  }
  
  /* 设置代码块样式 */
  pre {
    background-color: #f7f7f7;
    padding: 1em;
    border-radius: 4px;
    overflow: auto;
  }
  
  code {
    font-family: Consolas, Monaco, Courier, monospace;
    font-size: 0.9em;
    background-color: #f7f7f7;
    padding: 0.2em 0.4em;
    border-radius: 4px;
  }
  
  /* 设置引用样式 */
  blockquote {
    margin: 0;
    padding-left: 1em;
    border-left: 4px solid #ddd;
    color: #777;
  }
  
  /* 设置表格样式 */
  table {
    border-collapse: collapse;
    width: 100%;
    margin-bottom: 1.3em;
  }
  
  table th,
  table td {
    padding: 0.5em;
    border: 1px solid #ccc;
  }
  
  /* 添加一些额外的样式,如图片居中显示 */
  img {
    display: block;
    margin: 0 auto;
    max-width: 100%;
    height: auto;
  }
  
  /* 设置代码行号样式 */
  pre code .line-numbers {
    display: inline-block;
    width: 2em;
    padding-right: 1em;
    color: #999;
    text-align: right;
    user-select: none;
    pointer-events: none;
    border-right: 1px solid #ddd;
    margin-right: 0.5em;
  }
  
  /* 设置代码行样式 */
  pre code .line {
    display: block;
    padding-left: 1.5em;
  }
  
  /* 设置代码高亮样式 */
  pre code .line.highlighted {
    background-color: #f7f7f7;
  }
  
  /* 添加一些响应式样式,适应移动设备 */
  @media only screen and (max-width: 768px) {
    body {
      font-size: 14px;
      line-height: 1.5;
    }
    
    h1 {
      font-size: 1.8em;
    }
    
    h2 {
      font-size: 1.5em;
    }
    
    h3 {
      font-size: 1.3em;
    }
    
    h4 {
      font-size: 1.1em;
    }
    
    h5 {
      font-size: 1em;
    }
    
    h6 {
      font-size: 0.9em;
    }
    
    table {
      font-size: 14px;
    }
  }    
新建一个md文件

README.md

 # 欢迎使用 Marked

## 我是果冻桑

```JavaScrip
const fs = require('fs');
const marked = require('marked');

// 读取 Markdown 文件
fs.readFile('readme.md', 'utf8', (err, data) => {
    你好我是果冻桑, 今天我们来讲一下小米 MIX3 的故事
});
```
app.js 启动

创建browser 并且开启一个服务 设置根目录和 index.html 文件

const browserSync = require('browser-sync')
const openBrowser =  () => {
    const browser = browserSync.create()
    browser.init({
        server: {
            baseDir: './',
            index: 'index.html',
        }
    })
    return browser
}    

完整代码 ( 历史或者后续版本可以通过 gitte 进行查看 )

目录:

// 下面是对 app2.js 进行进一步的优化 封装 
const { error, time } = require('console')
const ejs = require('ejs')  // 引入 ejs 模板引擎
const fs = require('fs')  // 文件读写
const marked = require('marked') //  Markdown 转 HTML
const readme = fs.readFileSync('README.md') // 读取 README.md 文件
const readmePath = 'README.md'
// 具体步骤: 
// - 读取文件 readme.md 
// - 把 markdown 转换为 HTML 
// - 把上面的 html 嵌入到 使用 template.ejs 模版  => 形成完整的html
// ( 具体可以对比下面的 content 以及 data 的打印出来的)

// 1. 优化一: 去掉回调函数
const utils = require('util')
const ejsrenderFilePromise = utils.promisify(ejs.renderFile)

const content = marked.parse(readme.toString());
// console.log(content)
const parameters = {
    content: content,
    title: "markdown to html",
}

// 2. 启动浏览器 函数
let browser
const browserSync = require("browser-sync"); // 导入browser-sync库,用于实时预览和同步浏览器
const { resolve } = require('path')
const { rejects } = require('assert')
const openBrowser = async () => {
  const browser = browserSync.create();
  browser.init({ 
    server: {
      baseDir: "./",
      index: "index.html",
    },
  });
  return browser;
};

// 写入 index.html 函数
let path = "index.html" // 路径
const writeFile = async (html)=>{
    return new Promise((resolve,reject) =>{
        let writeStream = fs.createWriteStream(path)
        writeStream.write(html)
        writeStream.close()
        writeStream.on('finish', resolve)
        writeStream.on('error', reject)
    })
}

// 3. 新增热更新功能 函数
const generateHtmlAndReloadBrowser = async () => {
    const readmeContent = fs.readFileSync(readmePath);
    const content = marked.parse(readmeContent.toString());
    parameters.content = content; // 更新参数
    const Allhtml = await ejsrenderFilePromise('template.ejs', parameters);
    await writeFile(Allhtml);
    if (browser) {
        browser.reload(); // 重新加载浏览器
    }
};

// 监听文件变化
const watchReadmeFile = () => {
    // fs.watchFile(readmePath, (curr, prev) => {
    //     if( curr.mtime !== prev.mtime) {
    //         generateHtmlAndReloadBrowser(); // 文件变化时重新生成 HTML 并重新加载浏览器
    //     }
    // });
    const debouncedGenerateHtmlAndReloadBrowser = debounce(generateHtmlAndReloadBrowser, 1000);
    fs.watch(readmePath, (eventType, filename) => {
        // console.log(`File ${filename} has been ${eventType}, regenerating HTML...`);
        // generateHtmlAndReloadBrowser(); // 文件变化时重新生成 HTML 并重新加载浏览器
        // 防抖
        debouncedGenerateHtmlAndReloadBrowser()
    });
};

// 防抖函数 
function debounce(func ,wait) {
    let timeout
    return function executeFunction(...args) {
        const later = ()=>{
            clearTimeout(timeout);
            func(...args)
        }
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    }
}

// 启动
const generateHtmlAndOpenBrowser = async () => {
    // 返回是完整 html 
    const Allhtml = await ejsrenderFilePromise('template.ejs', parameters)
    await writeFile(Allhtml)
    browser = await openBrowser()  // 启动浏览器
    
}

generateHtmlAndOpenBrowser()
  .then(watchReadmeFile)
  .catch((err) => {
    console.error(err); // 使用 console.error 打印错误信息
  });
代码分析:

总体思路:

  • 通过 fs.readFileSync 方式读取文件,获取 readme.md 的文件
  • 通过 marked.parse(readme.toString())markdown 转为 html
  • 通过工具类中 promisify 的方式,ejs.renderFile 实现返回 Promise
  • const Allhtml = await ejsrenderFilePromise("template.ejs", parameters);把转化后的html 以参数的形式嵌入到 JavaScript模板引擎,其中 parameters 就是上面第二步的获取的html
  • 通过函数 writeFile 函数写入 ,如 index.html
  • 启动浏览器
  • 监听浏览器,如果 readme.md 文件有变化,就会进行重新写入以及刷新浏览器的操作
  • 简单优化一下,加入防抖的函数,避免多次更改导致多次写入
  1. 读取
const readme = fs.readFileSync('README.md') 

这行代码使用 fs 模块的 readFileSync 方法同步读取 README.md 文件的内容,

以 Buffer 对象的形式存储在 readme 变量中。如果需要字符串形式,可以指定编码,例如 fs.readFileSync('README.md', 'utf8')。当然可以使用 toStirng 方法

  1. toString

A. 基本数据类型转换

对于原始值(如数字、字符串、布尔值等),toString() 方法可以将它们转换为字符串。

const num = 10;
const strNum = num.toString(); // "10"

const bool = true;
const strBool = bool.toString(); // "true"

const undef = undefined;
const strUndef = undef.toString(); // "undefined"

const nullValue = null;
const strNull = nullValue.toString(); // "null" 

B. 对象转换

对于对象,toString() 方法返回一个字符串,该字符串表示对象的类型和在内存中的地址。通常这不是一个有用的字符串,因为它不提供对象的内容信息。

const obj = { name: "大钊" };
const strObj = obj.toString(); // "[object Object]"

C. 数组 字符串 的 toString

const arr = [1, 'hello', true];
const strArr = arr.toString(); // "1,hello,true"

const strObj = new String("Hello");
const strStrObj = strObj.toString(); // "Hello"

D. 自定义 toString 方法

你可以在自定义对象中重写 toString() 方法,以提供更有用的字符串表示。

const obj = {
    name: "hello",
    toString: function() {
        return `Name: ${this.name}`;
    }
};
const strObj = obj.toString(); // "Name: hello"

3. 监听文件 fs.watch

fs.watch(filename, [options], [listener])
  • filename:要监听的文件或目录的名称。

  • options:(可选)一个字符串或对象,控制监听的行为。

    • 如果是字符串,它指定被监听的事件类型,可以是 'rename''change'

    • 如果是对象,它包含以下属性:

      • persistent:(默认为 true)如果设置为 false,则在发生事件后停止监听。
      • recursive:(默认为 false)如果设置为 true,则监听一个目录及其所有子目录。
      • encoding:(默认为 'buffer')文件名的编码。
  • listener:(可选)一个回调函数,当文件发生变化时被调用。

const fs = require('fs');

// 监听文件的变化
fs.watch('example.txt', (eventType, filename) => {
  if (eventType === 'rename') {
    console.log('文件被重命名或删除:', filename);
  } else if (eventType === 'change') {
    console.log('文件内容发生变化:', filename);
  }
});
  • 如果你需要更可靠的文件监听功能,可以考虑使用第三方库,如 chokidar
  1. 防抖
// 防抖函数
function debounce(func, wait) {
  let timeout;
  return function executeFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

监听文件变化

// 监听文件变化
const watchReadmeFile = () => {
  const debouncedGenerateHtmlAndReloadBrowser = debounce(
    generateHtmlAndReloadBrowser,
    1000
  );
  fs.watch(readmePath, (eventType, filename) => {
    debouncedGenerateHtmlAndReloadBrowser();
  });
};

理解:

A. 最简单结构最不好的防抖:

let t = null // 全局变量
onInput.oniput = function() {
    if(t!= null) clearTimeout(t)
    t = setTimeout(()=>{
        console.log(111)
    },1000)
}

B. 改善代码结构

// 防抖函数
// 返回值是一个函数 
function debounce(func, wait) {
  let timeout;
  return function executeFunction(...args) {
    const later = () => {
      func(...args); // 这里 ...args就是返回函数后,调用时传递的所有参数
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

结合监听事件addEventListener, 可以写一个箭头函数,传入参数

简单案例:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>防抖的简单理解</title>
  </head>
  <body>
    <button class="btn">点击</button>
  </body>
  <script>
    let btn = document.querySelector(".btn");
    function test(...args) {
      console.log("防抖");
      console.log(...args)
    }
    btn.addEventListener("click", ()=>debounce(test, 1000)(1,2,3,4))
    
    
    // 防抖函数
    function debounce(func, wait) {
      let timeout;
      return function executeFunction(...args) {
        const later = () => {
          console.log(...args)
          func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
      };
    }

  </script>
</html>

关于 this 指向问题分析

先看一个案例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>防抖的简单理解</title>
  </head>
  <body>
    <button class="btn">点击</button>
  </body>
  <script>
    let btn = document.querySelector(".btn");
    btn.addEventListener("click", ()=>{
      console.log("箭头函数",this)  // Windows
   })
    
    btn.addEventListener("click",function(e) {
      console.log("普通函数",this)  // 指向 btn
    })

  </script>
</html>

分析:

普通函数中的 this: 普通函数的 this 指向调用它的上下文。在这个例子中,普通函数是作为 addEventListener 的回调函数被调用的,而 addEventListenerbtn 元素的方法。因此,当点击事件发生时,这个普通函数的 this 指向调用它的对象,即 btn 元素。

箭头函数中的this

箭头函数不创建自己的 this 上下文,因此它没有自己的 this 值。箭头函数内部的 this 值由外围最近一层非箭头函数的执行上下文决定

所以当大家说“箭头函数没有 this”时,他们的意思是箭头函数不创建自己的 this 上下文,this 是继承自外围作用域

而说箭头函数有this的情况,则是说继承自外围作用域的this。这个this不是箭头函数自己的,而是继承来的

下面我们分析一下防抖这段代码中this指向问题

btn.addEventListener("click", debounce(test, 1000))
// 3. 点击后,调用函数  debounce(test, 1000) 也就是 返回的 executeFunction 函数
// 因为 executeFunction  是一个普通函数, 所以此时 this 指向应该是 btn 元素
function debounce(func, wait) {
      let timeout;
      return function executeFunction(...args) {  
        const later = () => {
          console.log(this)  
          func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);  
        //1. 由于 later 是箭头函数 ,
        //所以 this 指向外围作用区域 this 也就是 executeFunction 的this
        //2. 由于 executeFunction 的this 和执行有关, 所以看到上面第一行
      };
    }

如果有参数:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>防抖的简单理解</title>
  </head>
  <body>
    <button class="btn">点击</button>
    <button id="debounceButton">点击我</button>
  </body>
  <script>
    let btn = document.querySelector(".btn");
    function test(...args) {
      console.log(...args);
      console.log("test", this);
    }

    // 创建一个防抖版本的test函数
    const debounceTest = debounce(function () {
      test.call(this, "test");
    }, 1000);
    btn.addEventListener("click", debounceTest);

    function handleClick(message) {
      console.log(message);
      console.log("handleClick", this); // 这里的 this 指向按钮
    }

    const debounceClick = debounce(function () {
      // console.log("debounceClick",this)  // 从下面的 func.apply(context,args)
      // 可以知道 这里的 this 在点击后, 绑定的 button 元素 
      // 如果下面 只是调用 
      handleClick(this, "按钮被点击了(没有call,隐式调用 this 绑定在 window 中)")          
      handleClick.call(this, "按钮被点击了!(有call, this 绑定在 btn 元素中 )"); // 使用 call 将 this 绑定到按钮
    }, 1000)
    
    document.getElementById("debounceButton").addEventListener("click", debounceClick);

      
    // 防抖函数
    function debounce(func, wait) {
      let timeout;
      return function executeFunction(...args) {
        const context = this      
        const later = () => {
          func.apply(context, args); 
          console.log("executeFuction",...args)
          // func.apply(this, args)  // 这也是一样的,因为箭头函数没有this 
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
      };
    }

    //   btn.addEventListener("click", ()=>{
    //     console.log("箭头函数",this)  // Windows
    //  })

    //   btn.addEventListener("click",function(e) {
    //     console.log("普通函数",this)  // 指向 btn
    //   })
  </script>
</html>

示例一: ( 有参数 )

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>防抖按钮示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #f4f4f4;
        }
        button {
            padding: 10px 20px;
            font-size: 16px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <button id="debounceButton">点击我</button>

    <script>
        function debounce(fn, delay) {
            let timeout;
            return function(...args) {
                const context = this; // 保存当前上下文
                clearTimeout(timeout);
                timeout = setTimeout(() => fn.apply(context, args), delay);
            };
        }
        function handleClick(message) {
            console.log(message);
            console.log("handleClick", this); // 这里的 this 指向按钮
        }

        const debounceClick = debounce(function() {
            handleClick.call(this, '按钮被点击了!'); // 使用 call 将 this 绑定到按钮
        }, 1000);

        document.getElementById('debounceButton').addEventListener('click', debounceClick);
    </script>
</body>
</html>

示例二: ( 在 绑定事件传递参数 )

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Debounce with ...args Example</title>
  </head>
  <body>
    <button id="myButton">Click Me!</button>
  </body>
  <script>
    // 防抖函数
    function debounce(func, wait) {
      let timeout;
      return function executeFunction(...args) {
        const context = this;
        console.log("executeFunction", this)
        const later = () => {
          func.apply(context, args);
          console.log("executeFunction", ...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
      };
    }

    // 按钮点击事件处理函数,接受任意数量的参数
    function handleClick(...args) {
      console.log(this)
      console.log("Button clicked with args:", ...args);
    }

    // 获取按钮元素
    const button = document.getElementById("myButton");

    // 创建防抖包装后的事件处理函数
    const debouncedHandleClick = debounce(handleClick, 2000);

    // 给按钮添加点击事件监听器
    button.addEventListener("click", function (event) {
      console.log("addEventListener", this);  
      return debouncedHandleClick(event, "arg1", "arg2", "arg3");
      // return debouncedHandleClick.apply(this,event, "arg1", "arg2", "arg3");
    });
  </script>
</html>