Node.js 第二十四章:构建实时 Markdown 预览工具(EJS + Marked + BrowserSync)
2026/4/24 9:53:36 网站建设 项目流程

1. 为什么需要实时 Markdown 预览工具

作为一个经常写技术文档的开发者,我深刻体会到在编写 Markdown 文档时实时预览的重要性。想象一下这样的场景:你正在撰写一篇技术博客,反复在编辑器和浏览器之间切换查看效果,每次保存后都要手动刷新页面,这种体验实在太糟糕了。这就是为什么我们需要一个能够实时预览 Markdown 的工具。

Markdown 本身是一种轻量级标记语言,它的语法简单直观,但转换成 HTML 后的最终效果往往需要反复确认。特别是在处理复杂格式时,比如表格、代码块或者嵌套列表,实时预览能让我们立即看到效果,大大提高写作效率。

我尝试过很多 Markdown 编辑器,但作为一个 Node.js 开发者,我更希望能自己搭建一个符合个人需求的工具。这就是为什么我们要使用 EJS、Marked 和 BrowserSync 这三个强大的库来构建自己的解决方案。EJS 负责 HTML 模板渲染,Marked 处理 Markdown 转换,BrowserSync 则提供实时刷新功能,三者配合堪称完美。

2. 项目环境搭建与依赖安装

2.1 初始化 Node.js 项目

首先,我们需要创建一个新的 Node.js 项目。打开终端,创建一个新目录并初始化项目:

mkdir markdown-previewer cd markdown-previewer npm init -y

这个命令会创建一个基本的 package.json 文件。接下来,我们需要安装项目依赖:

npm install ejs marked browser-sync

这三个包就是我们的核心依赖:

  • ejs:强大的 JavaScript 模板引擎
  • marked:高效的 Markdown 解析器
  • browser-sync:实时浏览器同步工具

2.2 项目目录结构

我建议采用以下目录结构,这是我经过多次实践后总结出来的最佳方案:

markdown-previewer/ ├── node_modules/ ├── public/ │ ├── css/ │ │ └── style.css │ └── js/ ├── views/ │ └── template.ejs ├── README.md ├── index.js └── package.json

这种结构清晰明了,将静态资源放在 public 目录,模板文件放在 views 目录,主逻辑放在 index.js 中。

3. 核心功能实现

3.1 使用 Marked 解析 Markdown

Marked 是一个高效的 Markdown 解析器,使用起来非常简单。我们先来看基本用法:

const marked = require('marked'); const fs = require('fs'); const markdownContent = fs.readFileSync('README.md', 'utf8'); const htmlContent = marked.parse(markdownContent);

但实际项目中,我们可能需要对 Marked 进行一些自定义配置。比如设置代码高亮、自定义渲染器等:

marked.setOptions({ highlight: function(code, lang) { // 这里可以集成 highlight.js 等代码高亮库 return code; }, breaks: true, // 将换行符转换为 <br> gfm: true // 启用 GitHub Flavored Markdown });

3.2 EJS 模板引擎的使用

EJS 是我们用来生成最终 HTML 的模板引擎。首先创建一个模板文件 views/template.ejs:

<!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="/css/style.css"> </head> <body> <div class="container"> <%- content %> </div> </body> </html>

注意这里使用了两种不同的 EJS 标签:

  • <%= %>用于输出转义后的内容
  • <%- %>用于输出原始 HTML(这正是我们需要的)

在 JavaScript 中渲染这个模板:

const ejs = require('ejs'); ejs.renderFile('views/template.ejs', { title: 'Markdown Preview', content: htmlContent }, (err, result) => { if (err) throw err; fs.writeFileSync('public/index.html', result); });

3.3 集成 BrowserSync 实现实时刷新

BrowserSync 可以让我们的开发体验更加流畅。配置如下:

const browserSync = require('browser-sync'); const bs = browserSync.create(); bs.init({ server: { baseDir: './public', index: 'index.html' }, watch: true, files: ['README.md', 'views/**/*.ejs', 'public/css/*.css'] });

这里有几个关键点:

  • 设置 baseDir 为 public 目录
  • 启用 watch 模式
  • 监控 Markdown 文件、模板文件和 CSS 文件的变更

4. 完整实现与优化

4.1 完整的项目代码

现在我们把所有部分组合起来,创建一个完整的解决方案。下面是 index.js 的完整代码:

const fs = require('fs'); const path = require('path'); const ejs = require('ejs'); const marked = require('marked'); const browserSync = require('browser-sync'); // 配置 marked marked.setOptions({ breaks: true, gfm: true }); // 初始化 BrowserSync const bs = browserSync.create(); function convertMarkdown() { try { const markdown = fs.readFileSync('README.md', 'utf8'); const html = marked.parse(markdown); ejs.renderFile(path.join(__dirname, 'views/template.ejs'), { title: 'Markdown Preview', content: html }, (err, result) => { if (err) throw err; fs.writeFileSync(path.join(__dirname, 'public/index.html'), result); bs.reload(); }); } catch (err) { console.error('Error:', err); } } // 启动 BrowserSync bs.init({ server: { baseDir: './public', index: 'index.html' }, watch: true, files: ['README.md', 'views/**/*.ejs', 'public/css/*.css'] }); // 初始转换 convertMarkdown(); // 监听文件变化 fs.watchFile('README.md', () => { console.log('Markdown file changed, rebuilding...'); convertMarkdown(); });

4.2 添加样式美化输出

为了让 Markdown 渲染结果更美观,我们需要添加一些 CSS 样式。在 public/css/style.css 中添加:

body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 20px; } h1, h2, h3, h4, h5, h6 { margin-top: 1.5em; margin-bottom: 0.5em; font-weight: 600; } h1 { font-size: 2em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; } h2 { font-size: 1.5em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; } h3 { font-size: 1.25em; } h4 { font-size: 1em; } h5 { font-size: 0.875em; } h6 { font-size: 0.85em; color: #777; } code { font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background-color: rgba(27,31,35,0.05); border-radius: 3px; padding: 0.2em 0.4em; font-size: 85%; } pre { background-color: #f6f8fa; border-radius: 3px; padding: 16px; overflow: auto; line-height: 1.45; } pre code { background-color: transparent; padding: 0; } blockquote { border-left: 4px solid #dfe2e5; color: #6a737d; padding: 0 1em; margin-left: 0; } table { border-collapse: collapse; width: 100%; margin-bottom: 16px; } table th, table td { padding: 6px 13px; border: 1px solid #dfe2e5; } table tr { background-color: #fff; border-top: 1px solid #c6cbd1; } table tr:nth-child(2n) { background-color: #f6f8fa; } img { max-width: 100%; box-sizing: content-box; background-color: #fff; }

4.3 添加开发脚本

在 package.json 中添加一个开发脚本,方便启动项目:

{ "scripts": { "dev": "node index.js" } }

现在只需要运行npm run dev就可以启动开发服务器了。每次修改 README.md 文件,浏览器都会自动刷新显示最新结果。

5. 高级功能扩展

5.1 添加代码高亮支持

虽然我们的预览工具已经可以工作,但代码高亮效果还不够好。我们可以集成 highlight.js 来增强代码显示效果:

首先安装 highlight.js:

npm install highlight.js

然后修改 marked 的配置:

const hljs = require('highlight.js'); marked.setOptions({ highlight: function(code, lang) { if (lang && hljs.getLanguage(lang)) { return hljs.highlight(lang, code).value; } return hljs.highlightAuto(code).value; } });

最后在模板中添加 highlight.js 的 CSS:

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/styles/github.min.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/highlight.min.js"></script> <script>hljs.highlightAll();</script>

5.2 支持多文件预览

目前我们的工具只能预览 README.md 文件。我们可以扩展它,使其能够预览任意 Markdown 文件。修改 index.js:

const chokidar = require('chokidar'); // 监听所有 .md 文件 const watcher = chokidar.watch('**/*.md', { ignored: /(^|[\/\\])\../, // 忽略点文件 persistent: true }); watcher .on('add', path => convertMarkdown(path)) .on('change', path => convertMarkdown(path)) .on('unlink', path => console.log(`File ${path} has been removed`)); function convertMarkdown(filePath = 'README.md') { // ...原有逻辑... }

记得安装 chokidar:

npm install chokidar

5.3 添加主题切换功能

为了让预览更加个性化,我们可以添加主题切换功能。首先创建几个不同的 CSS 主题文件,然后在模板中添加切换按钮:

<div class="theme-switcher"> <button>document.querySelectorAll('.theme-switcher button').forEach(btn => { btn.addEventListener('click', () => { const theme = btn.dataset.theme; document.getElementById('theme-style').href = `/css/themes/${theme}.css`; }); });

6. 实际应用中的经验分享

在真实项目中使用这个工具一段时间后,我总结出几个实用的技巧。首先是关于文件监听的稳定性问题。Node.js 原生的 fs.watch 在某些系统上可能不太可靠,这就是为什么我推荐使用 chokidar 这个库,它提供了更强大的文件监听功能。

另一个常见问题是 Markdown 文件的编码。有些编辑器会使用 UTF-8 with BOM 编码保存文件,这可能导致解析出错。解决方法是在读取文件时明确指定编码:

fs.readFileSync('README.md', 'utf8');

关于性能优化,当 Markdown 文件很大时,频繁的解析和渲染可能会影响响应速度。可以考虑添加防抖功能,避免短时间内多次触发转换:

let debounceTimer; function convertMarkdownDebounced() { clearTimeout(debounceTimer); debounceTimer = setTimeout(convertMarkdown, 300); } fs.watchFile('README.md', convertMarkdownDebounced);

最后,如果需要在团队中共享这个工具,可以考虑将其打包成一个全局命令行工具。创建一个 bin 目录,添加可执行文件,然后在 package.json 中配置 bin 字段。这样团队成员只需要全局安装一次,就可以在任何地方使用这个 Markdown 预览工具了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询