得益于 vuepress 的强大功能,我们可以直接在 markdown 中写文章,然后自动渲染为文章内容。接下来实现一个简单的 vue-markdown-loader,将 .md 文件转换为文章内容。

webpack 是无法直接处理 .md 格式文件的,直接 import 会报错,此时需要一个 loader 来处理,这个 loader 的作用就是把 .md 文件转换为 html 格式,然后加上 <template></template> 标签丢给 vue-loader 处理。其实我们平常写的单文件组件 .vue 也是由 vue-loader 处理的。

新建一个 md-loader.js 文件,复制下面代码,一个最简单的 vue-markdown-loader 就完成了。

const md = require('markdown-it')()

module.exports = function (source) {
  this.cacheable()

  // source 是原始文件内容,html 是用 markdown-it 编译后的 html 内容
  const html = md.render(source)

  const template = (
    `<template>
      <div class="markdown-body">
        ${html}
      </div>
    </template>`
  )

  return template
}

# Loader 配置

loader 写好后还要在 webpack 配置文件中配置,下面例子是 vue.config.js 的配置。

const path = require('path')

module.exports = {
  chainWebpack: config => {
    config.module
      .rule('markdown')
      .test(/\.md$/)
      .use('vue-loader')
        .loader('vue-loader')
        .end()
      .use('md-loader')
        .loader(path.resolve(__dirname, 'src/loaders/md-loader.js'))
        .end()
  }
}

配置好了在组件中引用,这里假设 test.md 是一篇 md 文章。

<template>
  <Test />
</template>

<script>
import Test from './test.md'

export default {
  components: {
    Test
  }
}
</script>

# 代码高亮、自定义容器

接下来加上代码高亮、样式处理以及自定义容器。

const hljs = require('highlight.js')
const cheerio = require('cheerio')
const md = require('markdown-it')()
const mdContainer = require('markdown-it-container')

module.exports = function (source) {
  this.cacheable()

  md.set({
    linkify: true, // 将类似 URL 的文本自动转换为链接。
    html: false, // Enable HTML tags in source
    // 代码高亮
    highlight: function (str, lang) {
      if (lang && hljs.getLanguage(lang)) {
        try {
          return (
            '<pre class="hljs"><code>' +
            hljs.highlight(lang, str, true).value +
            '</code></pre>'
          )
        } catch (__) {}
      }

      return (
        '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'
      )
    }
  })

  // 自定义容器,:::tip 会转换为 <div class="tip">
  md.use(mdContainer, 'tip')
  md.use(mdContainer, 'warning')

  const html = md.render(source)

  const $ = cheerio.load(html, {
    decodeEntities: false,
    lowerCaseAttributeNames: false,
    lowerCaseTags: false
  })

  const style = $.html('style')

  $('style').remove()

  const temp = $.html()

  const template = (
    `<template>
      <div class="markdown-body">
        ${temp}
      </div>
    </template>
    ${style}`
  )

  return template
}

虽然 .md 文件已经能被正常解析了,但还是没有任何样式,需要导入两个样式文件。

# 自动注册路由

目前这种方式还需要手动引入 md 文件,然后注册组件并使用,还是有点麻烦,可以把一篇 md 文件自动注册为一个对应的组件,然后注册到路由中。

<!-- blog.vue -->
<template lang="html">
  <router-view></router-view>
</template>
// router.js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

// 遍历 blog 目录下所有 md 文件, 并生成路由
const blogRoutes = []
const blogs = require.context('@/blog', false, /\w+\.(md)$/)
blogs.keys().forEach(fileName => {
  const reg = /\.\/(.+).md/
  const name = fileName.match(reg)[1]
  blogRoutes.push({
    path: name,
    name: name,
    component: resolve => require([`@/blog/${name}.md`], resolve)
  })
})

const routes = [
  {
    path: '/blog',
    name: 'blog',
    component: resolve => require(['@/views/blog'], resolve),
    children: blogRoutes
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

此时在 blog 文件夹下面新建一个 test.md 文件,则自动生成一个 /blog/test 路由,直接访问即可。

# 批量注册全局组件

vuepress 中可以直接引用 .vuepress/components 中的组件,这得益于批量注册全局组件的能力。

// auto-register.js
// 自动注册全局组件
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

// 将components目录下所有子组件注册为全局组件
const requireComponent = require.context('@/components', false, /\w+\.(vue)$/)

requireComponent.keys().forEach(fileName => {
  // 获取组件配置
  const componentConfig = requireComponent(fileName)

  // 获取组件的 PascalCase 命名
  const componentName =
    upperFirst(
      camelCase(
        // 获取和目录深度无关的文件名
        fileName
          .split('/')
          .pop()
          .replace(/\.\w+$/, '')
      )
    )

  // 全局注册组件
  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 否则回退到使用模块的根。
    componentConfig.default || componentConfig
  )
})
// main.js
import './auto-register'