vuepress-plugin-demo-container之字符串渲染与解析


theme: cyanosis

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情

container的具体实现中,我们知道经过container.js的处理之后,得到了一个HTML字符串,那么接下来,我们的工作就是处理字符串,将其渲染为Vue代码。

    extendMarkdown: md => {
      const id = setInterval(() => {
        const render = md.render;
        if (typeof render.call(md, '') === 'object') {
          md.render = (...args) => {
            let result = render.call(md, ...args);
            // result.html 就是经过container.js处理的代码
            const { template, script, style } = renderDemoBlock(result.html);
            result.html = template;
            result.dataBlockString = `${script}\n${style}\n${result.dataBlockString}`;
            return result;
          }
          clearInterval(id);
        }
      }, 10);
    }

由上述代码片段可知,主要的字符串处理工作就是在 renderDemoBlock 函数中完成的,下面我们就一起探究这个函数具体完成了那些事情。

renderDemoBlock 函数解析

点击 renderDemoBlock 查看源文件

还原代码块中的代码

还记得上篇文章中说,代码块中的代码是以注释的形式存放在HTMl中,因此,第一步就是将注释代码还原为正常代码。

<demo-block>
  <template slot="demo">
<!--pre-render-demo:<template>
  <div class="red-center-text">
      <p>{{ message }}</p>
      <input v-model="message" placeholder="Input something..."/>
  </div>
</template>
<script>
export default {
  data() {
    return {
      message: 'Hello Vue'
    }
  }
}
</script>
<style>
.red-center-text { 
  color: #ff7875;
  text-align: center;
}
</style>
:pre-render-demo-->
  </template>
</demo-block>

通过查找开始标志和结束标志在字符串中的位置,将注释代码截取出来

  const startTag = '<!--pre-render-demo:';
  const startTagLen = startTag.length;
  const endTag = ':pre-render-demo-->';
  const endTagLen = endTag.length;
  ...
  ...
  let commentStart = content.indexOf(startTag);
  let commentEnd = content.indexOf(endTag, commentStart + startTagLen);
  const commentContent = content.slice(commentStart + startTagLen, commentEnd);

这里有一个不常用的知识点

str.indexOf(target) 可以获得目标字符串在 str 第一次出现的位置 ,它还可以传入第二个参数,用来限制从哪一个下标开始查找

const str = `hello,world`
const idx = str.indexOf('o') // 4
const idx1 = str.indexOf('o', 5) // 6

经过上述步骤就得到了代码块中的代码:

<template>
  <div class="red-center-text">
  <p>{{ message }}</p>
  <input v-model="message" placeholder="Input something..."/>
  </div>
</template>
<script>
export default {
  data() {
return {
  message: 'Hello Vue'
}
  }
}
</script>
<style>
.red-center-text { 
  color: #ff7875;
  text-align: center;
}
</style>

到了这一步,我们其实就得到了一个完整的Vue SFC (Single File Component)文件。接下来,就需要编译该文件。

编译 Vue SFC文件

首先我们要先了解一下 Vue SFC 文件最终会被编译为什么样子

在 Vue2 中,我们可以通过如下方式定义一个组件:

<div id="app"></div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
  <script>
    // js 对象
    const profileInstance = {
      data: function () {
        return {
          msg: 'hello'
        }
      },
      render: function (createElement) {
        return createElement('p', 'hello')
      }
    }
    const Profile = Vue.extend(profileInstance)
    new Profile().$mount('#app')
  </script>

这种形式其实就是 SFC 编译后的形式,我们需要将 vue 文件编译后成 js 对象的形式

分离template、script、style

由于 template、script、style需要不同的处理,我们需要将这三者从字符串中分离出来。

// 提取 script
function stripScript(content) {
  const result = content.match(/<(script)>([\s\S]+)<\/\1>/);
  return result && result[2] ? result[2].trim() : '';
}

// 提取style
function stripStyle(content) {
  const result = content.match(/<(style)\s*>([\s\S]+)<\/\1>/);
  return result && result[2] ? result[2].trim() : '';
}

// 提取 template
// 编写例子时不一定有 template。所以采取的方案是剔除其他的内容
function stripTemplate(content) {
  content = content.trim();

  if (!content) {
    return content;
  }
  return content.replace(/<(script|style)[\s\S]+<\/\1>/g, '').trim();
}

解析 template

从上文 js 对象中可以看出,HTML是通过一个render函数渲染出来的。因此,解析 template 的过程其实就是将 template 转换为 render 渲染函数的过程。

template 转换为 render 渲染函数是一个非常复杂的过程,需要经历如下阶段:

Vue.js Compiler.jpg

但幸运的是,Vue 团队已经帮我们完成了这项工作。

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

  const finalOptions = {
    source: `<div>${template}</div>`,
    filename: 'inline-component', 
    compiler
  };
  const compiled = compileTemplate(finalOptions);
  // compiled.code 就是render渲染函数

编译后的结果:

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c(
    "div",
    [
      [
        _c("div", { staticClass: "red-center-text" }, [
          _c("p", [_vm._v(_vm._s(_vm.message))]),
          _vm._v(" "),
          _c("input", {
            directives: [
              {
                name: "model",
                rawName: "v-model",
                value: _vm.message,
                expression: "message"
              }
            ],
            attrs: { placeholder: "Input something..." },
            domProps: { value: _vm.message },
            on: {
              input: function($event) {
                if ($event.target.composing) {
                  return
                }
                _vm.message = $event.target.value
              }
            }
          })
        ])
      ]
    ],
    2
  )
}
var staticRenderFns = []
render._withStripped = true

合并 script 和 template

script 和 template 在一块才是一个完整的 Vue 对象,因此,需要将二者合并起来

script = script.trim();
  if (script) {
    script = script.replace(/export\s+default/, 'const democomponentExport =');
  } else {
    script = 'const democomponentExport = {}';
  }
  demoComponentContent = `(function() {
    ${demoComponentContent}
    ${script}
    return {
      render,
      staticRenderFns,
      ...democomponentExport
    }
  })()`;

demoComponentContent 就是完整的 Vue 对象了,此时,经过打包等处理之后已经可以渲染到页面上了

style 此时并不需要处理,所有的style 都会交由 style-loader 进行处理

插槽代码替换

写到这里,千万不要忘记了我们以上处理的 Vue 代码都是通过 slot 嵌入到 demo-block 组件中的,如上所示

所以,我们还需要将编译处理过的 Vue 对象放入 <demo-block slot='demo'></demo-block>

这里,其实是通过构建组件的方法完成的。

const demoComponentName = `render-demo-${id}`; // 示例代码组件名称
// 将子组件存入到templateArr中
templateArr.push(`<template><${demoComponentName} /></template>`);
componenetsString += `${JSON.stringify(demoComponentName)}:${demoComponentContent},`;

pageScript = `<script>
  export default {
name: 'component-doc',
components: {
  ${componenetsString}
}
  }
</script>`;

每个 demo 代码块都有一个 id, 所以可以根据 id 创建一个独一无二的组件名称。

pageScript 是整个 markdown 文件对应的 Vue 对象,将示例代码组件作为子组件在 components 属性中进行注册,就可以通过 demoComponentName 使用子组件了。

<demo-block slot='demo'>
<!--这里id仅为示意,应该为数字 -->
<template>
<render-demo-id></render-demo-id>
</template>
</demo-block>

那么,这是如何进行替换的呢?

在处理传入 renderDemoBlock 的HTML 字符串时,首先会创建一个 templateArr 数组用来存储模版输出内容。

在还原注释代码一步中,会得到注释代码的起始位置和结束位置。

在处理 template 和 script 之前,首先会根据 commentStart 将注释代码之前的代码存储到 templateArr

templateAArr.push(content.slice(start, commentStart))

templatescript 处理结束后,会根据 commentEnd将注释代码之后的代码存储到 templateArr

templateArr.push(content.slice(commentEnd))

这样就将原本的注释代码,替换为了编译后的Vue子组件

到了这里我们就实现了HTML字符串解析的全过程。随着整个 markdown 文件 对应 Vue 对象被渲染,作为其子组件的 demo 代码也会被正常渲染,从而展示在界面上。

总结

本篇文章,详解的介绍了如何处理HTML字符串,如何处理代码块中的代码。

但以上所介绍的都是以单个 markdown 文件为基础,多个文件的情况并没有说明。其实,原理是相同的,只是多个文件需要循环处理 HTML 字符串

vuepress-plugin-dmeo-container 源码解析系列到此就结束了,希望可以帮助到使用该插件的其他同学!

vuepress-plugin-demo-container 源码解析系列

  1. 组件库中的示例代码是如何展示的呢?
  2. vuepress-plugin-demo-container之containers.js
© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容