vue展示md文件,前端读取展示markdown文件

Vue 中加载并渲染 Markdown 文件的两种方案(含批量生成侧边栏)

场景:项目里要做文档展示页 / 帮助中心 / 更新日志,内容以 .md 文件维护,前端负责渲染成页面。常见两种思路:

  • 方案一:构建时编译 —— webpack 配 markdown-loader,require('./xxx.md') 直接把 md 编译成 HTML 字符串打进 bundle,配合 v-html 渲染。缺点:md 改一次就得重新 build。
  • 方案二:运行时渲染 —— md 当静态资源放静态目录,运行时 axios 拉原文,交给 vue-markdown 组件渲染。md 改完刷新即见,不用重新构建。

方案一:markdown-loader + require + v-html(构建时编译)

1. 安装 loader

npm install --save markdown-loader html-loader

注意补上了 html-loader:markdown-loader 的输出是 HTML 字符串,不是合法的 JS 模块,需要串一层 html-loader 把它变成 module.exports = "<html 字符串>",require 回来的才是可用字符串。

2. webpack 配置

这一步原文漏了,没有它 require('./xxx.md') 会直接报错。vue-cli 2 写在 build/webpack.base.conf.js 的 module.rules 里;vue-cli 3+ 写在 vue.config.js:

// vue.config.js
module.exports = {
  chainWebpack: (config) => {
    config.module
      .rule('markdown')
      .test(/\.md$/)
      .use('html-loader')
      .loader('html-loader')
      .end()
      .use('markdown-loader')
      .loader('markdown-loader');
  },
};

3. 引入并渲染

const htmlMD = require('./xxx.md'); // 编译期已转好的 HTML 字符串

export default {
  data() {
    return { htmlMD };
  },
};
<div v-html="htmlMD"></div>

特点:

  • md 在打包时就变成了 HTML,运行时零解析开销;
  • 代价是 md 内容全部进 bundle,且每次改 md 都要重新打包——适合内容基本固定的文档(协议、说明页)。

原文在方案一里写的"也可以通过 ajax 获取资源对象"其实行不通:axios 拿到的是 md 原文(纯文本),v-html 渲染出来就是一坨没解析的 # 和 **。要运行时拉取 + 渲染,请直接看方案二。

方案二:vue-markdown + axios(运行时渲染)

1. 安装组件

npm install --save vue-markdown

原文误写成 vue-loader——那是解析 .vue 单文件组件的 loader,跟 md 渲染无关;这一步也不需要 markdown-loader。

2. 注册组件

import Vue from 'vue';
import VueMarkdown from 'vue-markdown';

Vue.component('vue-markdown', VueMarkdown);
// 或只在页面内用:components: { VueMarkdown }

3. 拉取并渲染

md 文件放静态资源目录(vue-cli 2 的 static/、vue-cli 3+ 的 public/),保证浏览器能直接按 URL 访问到:

export default {
  data() {
    return { mdSource: '' };
  },
  mounted() {
    axios.get('/static/docs/xxx.md').then((response) => {
      this.mdSource = response.data; // md 原文
    });
  },
};
<vue-markdown :source="mdSource"></vue-markdown>

特点:

  • md 就是普通静态文件,改完刷新即见,不用重新构建;
  • 运行时多一次请求 + 前端解析开销;
  • 按需加载,不会把所有文档打进 bundle。

代码高亮(两种方案通用)

默认渲染出来的代码块没有配色,套一个 highlight.js 即可:

npm install --save highlight.js
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';

// vue-markdown 透传 markdown-it 的配置
Vue.use(VueMarkdown, {
  highlight(str, lang) {
    if (lang && hljs.getLanguage(lang)) {
      return hljs.highlight(str, { language: lang }).value;
    }
    return str;
  },
});

再引一个 github-markdown-css,整篇文档的排版就贴近 GitHub 的阅读体验了。

XSS 提醒

v-html 和 markdown 渲染组件都不会自动消毒 HTML。渲染本地自己维护的文档没问题;如果 md 来源包含用户输入(评论、投稿),务必先过 DOMPurify(或给 markdown-it 配白名单)再插入,否则 <script> / <iframe> 注入就是这么来的。

补充:require.context 批量读取 md,自动生成侧边栏

文档多的时候不可能一个个手写 require / URL。webpack 的 require.context 可以一次拿到整个目录的 md 文件清单:

// 拉取 @/static/docs 目录(含子目录)下所有 .md 文件
const filesMD = require.context('@/static/docs', true, /\.md$/);
// keys() 形如:['./basics/getting-started.md', './basics/install.md', './advanced/plugin.md']

const listDatas = filesMD.keys().map((path) => {
  const segments = path.replace(/^\.\/|\.md$/g, '').split('/');
  return {
    name: segments[segments.length - 1],  // 文件名(不含后缀),作菜单标题
    path,                                 // context 的 key,取内容靠它
    dir: segments.slice(0, -1).join('/'), // 所在子目录,用于分组建树
    children: [],
    showChild: false,
  };
});

对原文的三处修正:

  • 原文 listItemS[1] 只取路径的第二段,文件一进子目录名字就取错了;这里改为取最后一段;
  • 原文 @/../static/xxxxMD 这种绕出 src 的写法依赖项目结构:vue-cli 2 的 static/ 在根目录(可写相对路径 ../../static/docs);vue-cli 3+ 的静态资源在 public/,而 public/ 下的文件不走 webpack,require.context 够不着——要么把文档目录挪进 src,要么改为枚举 URL 方式(方案二);
  • 变量名 tmepListDatas 少了个字母,顺手改成 listDatas。

把平铺清单按目录组装成二级侧边栏:

function buildSidebar(list) {
  const groups = {};
  list.forEach((item) => {
    const dir = item.dir || '未分类';
    (groups[dir] = groups[dir] || []).push(item);
  });
  return Object.keys(groups).map((dir) => ({
    name: dir,
    showChild: false,
    children: groups[dir],
  }));
}

点击菜单项时取内容渲染:

// 方案一:require.context 拿到的就是编译好的模块(HTML 字符串)
handleClick(item) {
  this.htmlMD = filesMD(item.path);
}

// 方案二:require.context 只用来建菜单,内容仍走 axios 按 URL 拉原文
handleClick(item) {
  const url = `/static/docs/${item.path.replace('./', '')}`;
  axios.get(url).then((res) => (this.mdSource = res.data));
}

webpack 5 里 require.context 依然可用;换到 Vite 对应的写法是 import.meta.glob('/src/static/docs/**/*.md', { eager: true }),思路一致。

两种方案怎么选

方案一:markdown-loader 方案二:vue-markdown + axios
编译时机 构建时(打进 bundle) 运行时(请求 + 前端解析)
md 改动后 需要重新 build 刷新即见
请求开销 无(已打进 JS) 每篇一次请求
包体积 所有 md 都算进 bundle 只拉当前这篇
适用场景 内容固定、篇数少的说明页 文档多、更新频繁的帮助中心

一句话:内容不常动选方案一省事,内容频繁更新选方案二免打包;文档量大都可以配合 require.context 自动建菜单。

后话:本文是 Vue 2 + webpack 时代的做法,vue-markdown 组件目前已停止维护。Vue 3 项目可以直接用 markdown-it + v-html(记得消毒),或开箱即用的 @kangc/v-md-editor,思路与本文一致。


欢迎关注博主:微信公众号交流,不定时更新前端资源

posted on 2019-06-24 19:45  中文还在写码  阅读(20762)  评论(0)    收藏  举报