前端开发

Webpack 读取本地 Markdown 文件并进行预处理

02 月 23 日 2021 年

在开发 NetUnion 的官网页面时,有这样一个需求:读取本地目录下的新闻和博客文件,并在前端渲染,其中文件均为 Markdown 格式。

与全栈开发直接调用后端数据库不同的是,没有数据表字段来记录文件的不同属性,例如文件的题目、作者、撰写日期等,因此这些属性需要记录在 .md 文件当中。

这样的撰写方式是不是很熟悉?没错,不就是我正在写的 Hexo 博客中 .md 文件的编写格式嘛!

自动导入本地的 .md 文件

当然,首先要读取某个目录下已经撰写好的 .md 文件,才能对内容进行预处理。

但如果每撰写好一个新的新闻或博客文件,就得在代码中 require 出来,太过于麻烦且不现实,因此就需要自动导入的方法。

Webpack 提供了 require.context() 方法可以完美解决导入目录下所有文件的问题,该方法可以导入指定目录(也可以包括子目录)下指定格式的所有文件。关于此方法的更多细节可以在 Webpack 官方文档中了解

撰写代码自动读取 @/docs/blog/ 及其子目录下的所有 .md 文件如下所示,其中 blogFiles(key) 为文件存储的具体内容:

const blogFiles = require.context("@/docs/blog/", true, /\.md$/);
blogFiles.keys().forEach((key) => {
  console.log(blogFiles(key));
});

对 .md 文件进行预处理

参考 Hexo 博客的撰写格式,可以规定 NetUnion 官网的新闻和博客撰写格式如下:

---
title: ${title}
date: ${date}
author: ${author}
---

${main-text}

即用两个 --- 框住属性内容,在第二个 --- 下面为正文内容

那么首先,可以用 split() 方法根据 --- 及换行符将文章划分为长度不少于 3 (因为在正文中可能出现 ---)的数组 arr。其中 arr[0] 为空,arr[1] 存储有属性内容,arr[2] 及之后存储正文内容。

// content 为传入的 .md 文件内容
const contentArray = content.split(/---+\r?\n/g);

对属性内容的处理同样可以先使用 split() 方法按换行符拆分为数组。

const contentInfo = contentArray[1];
const contentInfoArray = contentInfo.split(/\r?\n/g);

值得一提的是,在上面两次按换行符分割时,我都使用了 /\r?\n/g 正则表达式。其含义是匹配 0 个或 1 个 \r 及 1 个 \n,直到结束。因为在 CRLF 行尾序列的文件中,换行符由 \r\n 表示;而在 LF 行尾序列中,换行符由 \n 表示。这样就确保了在 Windows 和 Unix 两种不同的系统上撰写的文件,其解析不会受行尾序列所影响。

接下来就可以提取属性对象了。这里使用 trim() 方法来删除属性名和属性值前后可能出现的多余空格。

const contentInfoItem = {};
for (let i = 0; i < contentInfoArray.length - 1; i++) {
  const contentInfoParamArray = contentInfoArray[i].split(":");
  let contentInfoParamValue = "";
  for (let n = 1; n < contentInfoParamArray.length; n++) {
    contentInfoParamValue += contentInfoParamArray[n] + ":";
  }
  contentInfoItem[contentInfoParamArray[0].trim()] = contentInfoParamValue
    .slice(0, -1)
    .trim();
}

对正文内容的处理就相当简单了,只需要把 arr[2] 及之后存储的内容用 ---\n 连接起来就可以了。

let contentText = contentArray[2];
if (contentArray.length > 3) {
  for (let i = 3; i < contentArray.length; i++) {
    contentText += "---\n";
    contentText += contentArray[i];
  }
}

将属性对象与正文内容合并为一个新的对象,解析就完成了!

const result = {
  ...contentInfoItem,
  content: contentText,
};

如果愿意,还可以在最后对格式进行一定规范,例如可以对 date 属性进行处理:

// 格式为 YYYY-MM-DD
if (result.date != null) {
  const dateArray = result.date.split("-");
  const dateYear = dateArray[0];
  let dateMonth = dateArray[1];
  let dateDay = dateArray[2];
  if (dateMonth.length == 1) {
    dateMonth = "0" + dateMonth;
  }
  if (dateDay.length == 1) {
    dateDay = "0" + dateDay;
  }
  result.date = dateYear + "-" + dateMonth + "-" + dateDay;
}

解析 .md 为 HTML

将结果中的正文内容交给给任意 .md 解析器就可以了,例如 markdown-it

const md = require("markdown-it")({
  linkify: false, // 一些设置,并不重要,下同
  breaks: false,
  typographer: true,
});
const htmlContent = md.render(result.content);

完整的解析文件在这里