Some time ago, I wrote a UI component library in the company, which requires a component description document. Our component documentation is typically written in AN MD file and rendered as a page for presentation. We first generated the front-end project configuration based on vue-CLI scaffolding, and then we loaded our extension via webPack configuration Loader.

Basis of preparation

In Webpack, each file is treated as a module, and different modules are parsed by configuring different loaders.

First we create an MD file and write this code:

I am a paragraph of textCopy the code

Then we import the MD file in the page and find the following error:

There is no proper loader to handle this file type, we need additional Loader support to parse this file.

As we know, Webpack only supports JS module parsing. For other types of modules, we need to introduce module processor (loader), such as style-loader and CSS-loader for parsing styles, vue-loader for parsing Vue single file components, And the MD-Loader we wrote about today.

The development process

Our requirement is to develop a Markdown document loader that supports rendering of Vue components, so that we can directly read MD files to generate Vue components for preview, so our development process is as follows:

Pay for the use of md files

We will create an MD-Loader folder under the project we created and write the following code first:

var MarkdownIt = require('markdown-it');
const md = new MarkdownIt();

module.exports = function (source) {
  const content = md.render(source);
  const code = `module.exports = The ${JSON.stringify(content)}`
  return code
}
Copy the code

Then configure to import mD-loader in vue.config.js:

// vue.config.js
const path = require("path");

function resolve(dir) {
  return path.join(__dirname, dir);
}

module.exports = {
  chainWebpack: config= > {
    // Use a custom loader
    config.module
      .rule("md-loader")
      .test(/\.md$/)
      .use("md-loader")
      .loader(resolve("./md-loader/index.js")) .end(); }},Copy the code

Supports Vue components

Above we have parsed and generated HTML returns through the markdown-it plug-in, allowing us to support rendering of MD files. So we now support writing Vue components in MD files, how do we do that? First let’s adjust our configuration in vue.config.js:

// vue.config.js
const path = require("path");

function resolve(dir) {
  return path.join(__dirname, dir);
}

module.exports = {
  chainWebpack: config= > {
    // Use a custom loader
    config.module
      .rule("md-loader")
      .test(/\.md$/)
      .use("vue-loader")
      .loader("vue-loader")
      .options({
        compilerOptions: {
          preserveWhitespace: false
        }
      })
      .end()
      .use("md-loader")
      .loader(resolve("./md-loader/index.js")) .end(); }},Copy the code

Then modify the MD-loader file, we consider the MD file as a single file component of Vue, so we adjust the export format as follows:

// ./md-loader/index.js

var MarkdownIt = require('markdown-it');
const md = new MarkdownIt();

module.exports = function (source) {
  const content = md.render(source);
  const code = `
    <template>
      <section class="demo-container">
        ${content}
      </section>
    </template>
        <script>
            export default {}
        </script>
  `;
  return code;
}
Copy the code
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <testMd />
  </div>
</template>

<script>
import testMd from "./test.md";

export default {
  name: 'App'.components: {
    testMd
  }
}
</script>
Copy the code

Run it again and we can import and render the MD file as normal:

Support for Vue built-in template declarations

We convert md files to Vue single files, which supports all functions of Vue single files. By default, we can declare global components in MD, so what if we want to write local components in MD? Let’s adjust the md file code we introduced:

I am a paragraph of text ::demo` `{{MSG}}    Copy the code

So let’s see how we do that. It’s very simple. It is to find the corresponding Vue template module, then mark it and extract it into local components for mounting.

Parse and mark Vue template locations

Markdown-it-chain and Markdown-it-Container are the tokens that markdown-it parses. You can see the tokens online.

// ./md-loader/config.js

// Support chain use
const Config = require("markdown-it-chain");
// Matches the content block to parse the contents of the ::: package
const mdContainer = require("markdown-it-container");

const config = new Config();
function containers(md) {
  md.use(mdContainer, "demo", {
    validate(params) {
      return params.trim().match(/^demo\s*(.*)$/);
    },
    render(tokens, idx) {
      Nesting === 1
      if (tokens[idx].nesting === 1) {
        // Check whether the fence is enclosed in the code block
        const content = tokens[idx + 1].type === "fence" ? tokens[idx + 1].content : "";
        // Return a code block wrapped around it and add a tag
        return `<demo-block> <! --demo-begin:${content}:demo-end-->
        `;
      }
      return "</demo-block>"; }}); md.use(mdContainer,"tip")
}

config.options
  .html(true)
  .end()

  .plugin("containers")
  .use(containers)
  .end();

const md = config.toMd();

module.exports = md;
Copy the code

Matches the code block content and adds it to the component

We extracted the built-in components after md parsing and saved them into the single-file components of Vue, and then handed the transformed files to the next loader (VUe-Loader) for parsing.

const fs = require("fs");
const path = require("path");
const md = require("./config");
const cacheDir = ".. /node_modules/.cacheDir";

function resolve(dir) {
  return path.join(__dirname, dir);
}

if(! fs.existsSync(resolve(cacheDir))) { fs.mkdirSync(resolve(cacheDir)) }module.exports = function(source) {
  // Get the converted contents of the MD file
  const content = md.render(source);

  const startTag = "<! --demo-begin:"; // Match the open label
  const startTagLen = startTag.length; 
  const endTag = ":demo-end-->"; // Matches the closing tag
  const endTagLen = endTag.length;

  let components = ""; // Storage component example
  let importVueString = ""; // Store importing component declarations
  let uid = 0; / / the demo of the uid
  const outputSource = []; // The output content
  let start = 0; // Start position of string

  let commentStart = content.indexOf(startTag);
  let commentEnd = content.indexOf(endTag, commentStart + startTagLen);

  while(commentStart ! = = -1&& commentEnd ! = = -1) {
    outputSource.push(content.slice(start, commentStart));
    // Get the code block content
    const commentContent = content.slice(
      commentStart + startTagLen,
      commentEnd
    );

    const componentNameId = `demoContainer${uid}`;
    // Write the file locally
    fs.writeFileSync(resolve(`${cacheDir}/${componentNameId}.vue`), commentContent, "utf-8");
    // Declare the content slot passed in
    outputSource.push(`<template slot="source"><${componentNameId} /></template>`);
    // Add the import declaration
    importVueString += `\nimport ${componentNameId} from '${cacheDir}/${componentNameId}'; `;
    // Add a component declaration
    components += `${componentNameId}, `;

    // Recalculate the next position
    uid++;
    start = commentEnd + endTagLen;
    commentStart = content.indexOf(startTag, start);
    commentEnd = content.indexOf(endTag, commentStart + startTagLen);
  }

  // Add later
  outputSource.push(content.slice(start));
  return `
    <template>
      <section class="demo-container">
        ${outputSource.join("")}
      </section>
    </template>
    <script>
      ${importVueString}
      export default {
        name: 'demo-container',
        components: {
          ${components}
        },
      }
    </script>
  `;
};
Copy the code

The final result

Since our generated code block will be mounted under the global component, we need to create a global component and import it. The code is also simple:

<template>
  <div class="demo-block">
    <slot name="source"></slot>
  </div>
</template>

<script>

export default {
  name: "DemoBlock"};</script>
Copy the code

We execute it again and the resulting render looks like this:

The implementation idea is actually very simple, there are many similar plug-in support on the web, in fact, we still want to implement a Webpack Loader.

Now that we have implemented markdown file support for Vue component rendering, we can also add more extension support for Markdown file demonstration, I won’t go into the details here. Finally, we suggest that you can see the Webpack document “Writing a Loader” section to learn how to develop a simple Loader.