elment-ui md文件配置

462 阅读3分钟

饿了么前端介绍的Markdown解析和扩展 地址

把 Markdown 当成 Vue 组件来使用。在文档中,我们还编写了许多示例去描述组件的用法。在编写示例代码时,还有这两个需求:

1、示例代码格与单文件组件保持一致

2、示例代码像组件一样在页面中渲染

原生的markdown并不具备这样的功能,要对进行特殊的定制化

markdown-it 原理

输入一串 markdown 代码,最后得到一串 html 代码,整体流程如下:

markdown代码 --> 解析器 --> token流 --> 渲染器 --> html

解析器最主要的两个block和inline两个规则

block规则 # 我是一个例子 得到 heading_open 、inline、 heading_close 三个 token

inline规则 我是一个例子 我们得到了 3 + 1 个 token: 内联 token 都有一个.children 属性,带有嵌套 token 流

渲染器(Renderer) 生成token流,传递给renderer,会遍历所有token,将每个token.type传递给定义相同名字规则,渲染器规则在md.renderer.rules[name]

md-loader流程图

md-loader配置
module:{
    rules:{
        {
            test: /\.md$/,
            use: [
                {
                    loader: 'vue-loader',
                },
                {
                    loader: path.resolve(__dirname, './../src/mdLoader/index.js')   //  md文件加载进来后,要做些操作
                }
            ]
        },
    }
}

创建test.md 并添加到路由
    # 测试一个实例
    :::demo  //自定义的块级容器,显示在页面的例子
    
    ```html    //编写html代码
    <template>
        <Button class='testClass' v-if='isShow'>
            <span>default</span>
        </Button>
    </template>
    <script>
        export default{
            data(){
                return{
                    isShow:false
                }
            }
        }
    </script>
    ```
    :::
创建一个demo-block组件 在全局引入组件

创建的demo-block,将生成的render函数插到slot的source,显示的代码插到slot的highlight。 可以自定义做效果,现在先简单实现点击显示和隐藏代码

<template>
  <div>
    <slot></slot>
    <slot name="source"></slot>
    <Button class="code_button" @click="codeSwitch">显示代码</Button>
    <slot name="highlight" v-if="isCode"></slot>
  </div>
</template>
<script>
export default {
  name: "demoBlock",
  data() {
    return {
      isCode: false
    };
  },
  methods: {
    codeSwitch() {
      this.isCode = !this.isCode;
    }
  }
};
</script>

所需要用到的插件:

const Config = require('markdown-it-chain'); //支持markdown-it的链式操作

const anchorPlugin = require('markdown-it-anchor'); // markdown锚点

const mdContainer = require('markdown-it-container'); //此插件识别一块容器,并指定如何返回,扩展性要比自己写md.renderer.rules返回值好用

const { compileTemplate } = require('@vue/component-compiler-utils');

const compiler = require('vue-template-compiler');

index.js文件

被loader加载的index文件

主要做了将经过规则处理过添加占位符,截取替换占位符的内容把template的内容用@vue/component-compiler-utils插件转换成render函数,和script一起合并成自执行函数的组件,最后返回vue格式代码

    
const {
  stripScript,
  stripTemplate,
  genInlineComponentText
} = require('./util');
const md = require('./config');

module.exports = function (source) {
  //source是test.md传过来的原始数据:# test.md
  const content = md.render(source);  //经过了containers,fence 规则的处理,添加了<!--ksc--demo  demo-block等块级标识
  //<!--ksc-demo: ${content} :ksc-demo--> 截取中间的content内容,转换成vue里template,script生成vue dom,做成组件引用
  const startTag = '<!--ksc-demo:';
  const startTagLen = startTag.length;
  const endTag = ':ksc-demo-->';
  const endTagLen = endTag.length;

  let componenetsString = '';
  let id = 0; // demo 的 id
  let output = []; // 输出的内容
  let start = 0; // 字符串开始位置

  let commentStart = content.indexOf(startTag); //  开始查找<!--ksc-demo:的位置
  let commentEnd = content.indexOf(endTag, commentStart + startTagLen); //从commentStart+startTag的length位置开始搜索:ksc-demo-->
  console.log('commentStart',commentStart)
  // commentStart !== -1开始循环, 条件不成立,循环结束
  while (commentStart !== -1 && commentEnd !== -1) {  
    output.push(content.slice(start, commentStart));  // 把<!--ksc-demo:之前的代码先存放在output里面
    const commentContent = content.slice(commentStart + startTagLen, commentEnd); //截取内容 组件的代码集
    const html = stripTemplate(commentContent); // 过滤返回template
    const script = stripScript(commentContent); // 过滤返回script
  
    let demoComponentContent = genInlineComponentText(html, script);  //返回自执行函数
    const demoComponentName = `kscDemo${id}`;
    //output:['<demo-block>',<template slot="source"><ksc-demo0/></template>,</demo-block><demo-block>,<template slot="source"><ksc-demo1/></template>,</demo-block>]
    output.push(`<template slot="source"><${demoComponentName} /></template>`); //插入到demo-block slot name='source' 里面
    //后面会添加components:{}  kscDemo:(function(){})()
    componenetsString += `${demoComponentName}: ${demoComponentContent},`; //ksc-demo组件 

    // 重新计算下一次的位置
    id++; //组件的id
    start = commentEnd + endTagLen; //开始的位置
    commentStart = content.indexOf(startTag, start);  // 开始查找start之后是否还有<!--ksc-demo:
    commentEnd = content.indexOf(endTag, commentStart + startTagLen);
  }

  // 仅允许在 demo 不存在时,才可以在 Markdown 中写 script 标签
  // todo: 优化这段逻辑
  let pageScript = '';
  if (componenetsString) {
    pageScript = `<script>
      export default {
        name: 'component-doc',
        components: {
          ${componenetsString}
        }
      }
    </script>`;
  } else if (content.indexOf('<script>') === 0) { // 硬编码,有待改善
    start = content.indexOf('</script>') + '</script>'.length;
    pageScript = content.slice(0, start);
  }
  output.push(content.slice(start));
  console.log('output',output)
  //output = <demo-block><template slot="source"><ksc-demo0/></template></demo-block><demo-block><template slot="source"><ksc-demo1/></template></demo-block>
  //组装成vue代码返回
  return `
    <template>
      <section class="ksc_dome">
        ${output.join('')}
      </section>
    </template>
    ${pageScript}
  `;
};
    
config.js

添加containers规则和fence规则,生成了markdown的实例,导出md

const Config = require('markdown-it-chain');  //支持markdown-it的链式操作
const containers = require('./containers');   // container-demo-open类型
const overWriteFenceRule = require('./fence');    //fence类型

const config = new Config();
config
  .options.html(true).end()
  .plugin('containers').use(containers).end();  //添加containers规则

const md = config.toMd();   //使用以上配置创建markdown-it实例
overWriteFenceRule(md);   //修改fence

module.exports = md;
util.js

//过滤出template的内容,script的内容,使用 @vue/component-compiler-utils 与 vue-template-compiler对template 的编译,把render,staticRenderFns,script合并成一个对象返回

const { compileTemplate } = require('@vue/component-compiler-utils');// 编译Vue当个文件或者组件
const compiler = require('vue-template-compiler');  // 与vue-loader一起使用,被它引用 将模板预编译为渲染函数  插件解释://https://zhuanlan.zhihu.com/p/114239056

function stripScript(content) {
  const result = content.match(/<(script)>([\s\S]+)<\/\1>/);  //匹配script中间的内容,在result[2]  
  return result && result[2] ? result[2].trim() : '';
}

// 编写例子时不一定有 template。所以采取的方案是剔除其他的内容
function stripTemplate(content) {
  content = content.trim();
  if (!content) {
    return content;
  }
  return content.replace(/<(script|style)[\s\S]+<\/\1>/g, '').trim();
}

// 暂时没找到类似写法, vue-loader源码中就是这样引入的,转换成组件
function genInlineComponentText(template, script) {
  const finalOptions = {
    source: `<div>${template}</div>`,
    filename: 'inline-component', // TODO:这里有待调整
    compiler  //必须传递compiler,确定所使用的版本
  };
  //compiled的数据结构
  // ast: {
  // },
  // code: 'var render = function() {\n' +
  //   '  var _vm = this\n' +
  //   '  var _h = _vm.$createElement\n' +
  //   '  var _c = _vm._self._c || _h\n' +
  //   '  return _c(\n' +
  // tips: [],
  // errors: []
  const compiled = compileTemplate(finalOptions); // 将原始代码编译为JavaScript代码
  let demoComponentContent = `${compiled.code}`;  // compiled.code是原始代码html转的render函数
  script = script.replace(/export\s+default/, 'const democomponentExport =');//export default 内容赋值给democomponentExport

  // 没有绑定动态数据会生成静态render,保存到staticRenderFns数组中, 使用自执行函数返回组件的的结果
  demoComponentContent = `(function() {
    ${demoComponentContent}
    ${script}
    return {
      render,
      staticRenderFns,  
      ...democomponentExport
    }
  })()`;
  return demoComponentContent;
}

module.exports = {
  stripScript,
  stripStyle,
  stripTemplate,
  genInlineComponentText
};
fence.js

配置type为fence规则,返回vue结构

// 覆盖默认的 fence 渲染策略
module.exports = md => {
  const defaultRender = md.renderer.rules.fence;
  //tokend的type和rules[fence]进行匹配,修改返回该规则的vue代码
  md.renderer.rules.fence = (tokens, idx, options, env, self) => {
    const token = tokens[idx];
    // 判断该 fence 是否在 :::demo 内
    const prevToken = tokens[idx - 1];
    const isInDemoContainer = prevToken && prevToken.nesting === 1 && prevToken.info.trim().match(/^demo\s*(.*)$/);
    //```后面是否为html,prevToken是上一个token判断是不是:::demo
    if (token.info === 'html' && isInDemoContainer) {
      //escapeHtml对字符串进行转义
      return `<template slot="highlight"><pre v-highlight><code class="html">${md.utils.escapeHtml(token.content)}</code></pre></template>`;
    }
    //返回默认的渲染方式
    return defaultRender(tokens, idx, options, env, self);
  };
};
containers.js

用markdown-it-container插件是专门为:::来配置规则的,扩展性更强

const mdContainer = require('markdown-it-container');   //此插件识别一块容器,并指定如何返回

module.exports = md => {
  md.use(mdContainer, 'demo', {
    validate(params) {
      //遇到:::demo 匹配进来  return返回验证是否通过, 返回true走render,否则直接跳过
      return params.trim().match(/^demo\s*(.*)$/);
    },
    render(tokens, idx) {
      // 匹配成功返回会生成一个数组
      const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
      //  如果::: 后面有字符 nesting就会变成1,否则就是-1, 上面正则匹配所以:::后面一定是demo
      if (tokens[idx].nesting === 1) {
        //m[1]是:::后面的信息,比如'demo test'会映射到slot default里面
        const description = m && m.length > 1 ? m[1] : '';

        //  如果:::内部有``` tokens[index+1].type, 就一定是fence,  content就是``` ```里的内容
        const content = tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : '';

        //把截取到的md内容包装一层 description是:::demo 后面的内容 
        return `<demo-block>
        ${description ? `<div>${md.render(description)}</div>` : ''}
        <!--ksc-demo: ${content}:ksc-demo-->
        `;
      }
      //nesting不等于1的时候,返回闭合标签
      return '</demo-block>';
    }
  });
  md.use(mdContainer, 'warning');
};