最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Webpack 写一个 markdown loader

    正文概述 掘金(米亚流年)   2021-04-25   349

    前段时间在公司内部写了个 UI 组件库,需要有组件说明文档。我们的组件文档一般都是用 md 文件书写,然后渲染成页面展示。我们首先基于 vue-cli 脚手架生成前端项目配置,然后我们通过 webpack 配置 loader 的方式加载我们的扩展。

    基础准备

    首先我们先创建一个 md 文件,并写下这样一段代码:

        我是一段文字
    

    然后在页面中引入这个 md 文件,我们会发现有以下报错:

    Webpack 写一个 markdown loader 错误信息的是说在解析模块时遇到了非法字符,没有一个合适的 loader 去处理这种文件类型,我们需要额外的 loader 支持去解析这个文件。

    我们知道,在 Webpack 内部是只支持 JS 模块解析的,对于其他类型的模块我们就需要引入模块处理器(loader),比如解析样式的 style-loader、css-loader,解析 Vue 单文件组件的 vue-loader,以及我们今天写的 md-loader。

    开发流程

    我们的需求是开发一个支持 Vue 组件渲染的 markdown 文档加载器,让我们能够直接读取 md 文件生成 Vue 组件进行预览,所以我们的开发流程如下:

    支付md文件的使用

    我们先在我们创建的项目下建立一个 md-loader 的文件夹,然后先写下如下代码:

    var MarkdownIt = require('markdown-it');
    const md = new MarkdownIt();
    
    module.exports = function (source) {
      const content = md.render(source);
      const code = `module.exports = ${JSON.stringify(content)}`
      return code
    }
    

    然后在 vue.config.js 中配置引入 md-loader:

    // vue.config.js
    const path = require("path");
    
    function resolve(dir) {
      return path.join(__dirname, dir);
    }
    
    module.exports = {
      chainWebpack: config => {
        // 使用自定义 loader
        config.module
          .rule("md-loader")
          .test(/\.md$/)
          .use("md-loader")
          .loader(resolve("./md-loader/index.js"))
          .end();
      },
    }
    

    支持 Vue 组件使用

    在上面我们已经通过插件 markdown-it 解析并生成 html 返回,使我们能够支持 md 文件的渲染展示。那么我们现在也在支持在 md 文件中写 Vue 组件,我们该怎么做呢? 首先我们先调整下我们在 vue.config.js 中的配置:

    // vue.config.js
    const path = require("path");
    
    function resolve(dir) {
      return path.join(__dirname, dir);
    }
    
    module.exports = {
      chainWebpack: config => {
        // 使用自定义 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();
      },
    }
    

    然后修改 md-loader 文件,我们将 md 文件看作是一个 Vue 的单文件组件,所以我们的导出格式调整如下:

    // ./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;
    }
    
    <template>
      <div id="app">
        <img  src="./assets/logo.png">
        <testMd />
      </div>
    </template>
    
    <script>
    import testMd from "./test.md";
    
    export default {
      name: 'App',
      components: {
        testMd
      }
    }
    </script>
    

    再次运行我们就能正常引入并渲染 md 文件了:

    Webpack 写一个 markdown loader

    支持 Vue 内置模板声明

    我们将 md 文件转化为 Vue 单文件使用,以上已经能支持 Vue 单文件的所有功能,默认支持 md 内声明全局组件的使用,那么如果我们想在 md 内部写局部组件呢? 我们调整下我们引入的 md 文件代码:

    我是一段文字
    
    :::demo
    
    ```vue
    <template>
      <div>
        测试 md 内置组件 -- <span class="text">{{ msg }}</span>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          msg: "Hello World !!!",
        };
      },
    };
    </script>
    <style scoped>
    .text {
      color: #f00;
    }
    </style>
    

    我们先分析下如何实现,原理很简单。就是找到对应的 Vue 模板模块,然后打上标记并提取成局部组件进行挂载。

    Webpack 写一个 markdown loader

    解析并标记 Vue 模板位置

    这里我们引入插件 markdown-it-chain 和 markdown-it-container,另外,对于 markdown-it 解析的 tokens 可以查看在线示例,以下是具体代码:

    // ./md-loader/config.js
    
    // 支持链式使用
    const Config = require("markdown-it-chain");
    // 匹配内容块,解析以 ::: 包裹的内容
    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) {
            // 判断是否包裹在代码块(fence)中
            const content = tokens[idx + 1].type === "fence" ? tokens[idx + 1].content : "";
            // 返回以代码块包裹,并添加标记
            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;
    

    匹配代码块内容,并添加到组件中

    我们将 md 解析后提取内置组件保存成 Vue 的单文件组件,然后将转化后的文件交给下一个 loader(vue-loader)进行解析。

    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) {
      // 获取 md 文件转化后的内容
      const content = md.render(source);
    
      const startTag = "<!--demo-begin:"; // 匹配开启标签
      const startTagLen = startTag.length; 
      const endTag = ":demo-end-->"; // 匹配关闭标签
      const endTagLen = endTag.length;
    
      let components = ""; // 存储组件示例
      let importVueString = ""; // 存储引入组件声明
      let uid = 0; // demo 的 uid
      const outputSource = []; // 输出的内容
      let start = 0; // 字符串开始位置
    
      let commentStart = content.indexOf(startTag);
      let commentEnd = content.indexOf(endTag, commentStart + startTagLen);
    
      while (commentStart !== -1 && commentEnd !== -1) {
        outputSource.push(content.slice(start, commentStart));
        // 获取代码块内容
        const commentContent = content.slice(
          commentStart + startTagLen,
          commentEnd
        );
    
        const componentNameId = `demoContainer${uid}`;
        // 将文件写入本地
        fs.writeFileSync(resolve(`${cacheDir}/${componentNameId}.vue`), commentContent, "utf-8");
        // 声明内容插槽传入
        outputSource.push(`<template slot="source"><${componentNameId} /></template>`);
        // 添加引入声明
        importVueString += `\nimport ${componentNameId} from '${cacheDir}/${componentNameId}';`;
        // 添加组件声明
        components += `${componentNameId},`;
    
        // 重新计算下一次的位置
        uid++;
        start = commentEnd + endTagLen;
        commentStart = content.indexOf(startTag, start);
        commentEnd = content.indexOf(endTag, commentStart + startTagLen);
      }
    
      // 后续内容添加
      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>
      `;
    };
    

    最终效果

    由于我们生成的代码块会挂载在全局组件 下,所以我们需要创建一个全局组件并引入,代码也很简单:

    <template>
      <div class="demo-block">
        <slot name="source"></slot>
      </div>
    </template>
    
    <script>
    
    export default {
      name: "DemoBlock",
    };
    </script>
    

    我们再次执行,最终实现的渲染效果如下:

    Webpack 写一个 markdown loader 实现思路其实很简单,网上也有很多类似的插件支持,实际上我们还是想自己实现一遍 Webpack Loader。

    以上我们已经实现了 markdown 文件支持 Vue 组件渲染了,我们还可以添加更多扩展支持 markdown 文件的展示,这里就不多做讲解了。 最后也建议大家可以看下 Webpack 文档《Writing a Loader》章节,学习开发简单 loader。


    起源地 » Webpack 写一个 markdown loader

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元