【Vue3 造轮子项目 】kaiteUI中利用Custom Block(自定义块)和vite实现代码渲染器

关于kaiteUI中通过json数据实现代码渲染器

前言

上礼拜笔者分享了搭建UI框架的技术栈,其中一个较为关键的技术——代码渲染器,由于篇幅问题放到了今天这篇博客中给大家做一个详细的分析解读。


在这里先分析一下上篇博客内容—— 基于vue3.0 + vite + TypeScript 实现一个UI框架 - kaiteUI,这篇主要是通过基于vue3.0 + vite + TypeScript技术框架来实现一个UI框架,笔者自己搭建框架的初衷是运用并巩固最近学习的vue3.0新特性。


技术栈

废话不多说,先给大家分享一下实现代码渲染器的关键技术栈。

  • VUE3.0 前端主流框架(react,vue,angular)之一,更适合开发灵活度高且复杂的应用
  • vite 是一个由原生 ESM 驱动的 Web 开发构建工具
  • compiler-core 核心作用是将字符串转换成 抽象对象语法树AST
  • json 是一种轻量级的数据交换格式
  • prismjs 一款轻量级、可扩展的语法高亮显示工具

需求分析

在思考需求分析之前我们先来看看kaiteUI官网的效果演示:

在这里插入图片描述
由上面的图我们可以分析出,kaiteUI官网主要又以下几个部分组成:

  1. 属性功能概述(title
    主要展示组件当前属性的属性关键字
  2. 组件展示部分(componentShow
    负责将指定属性的组件展示,并且让用户能够与其进行交互。
  3. 代码展示模块(demoCode
    对各个组件代码示例进行展示,并给出使用示例源码,主要方便使用者了解各个组件的使用方法。

技术实现

实现原理

笔者大致总结的流程如下图所示:
在这里插入图片描述

关键技术

vue3.0中的自定义块

这边用到的关键技术是在vite官网看到的一个文档,上面表示——Vite支持Vue SFC中的自定义块。只需要在配置文件中给vueCustomBlockTransforms自定义块添加一系列的转换函数即可使用。

官方给的代码示例为:

// vite.config.js
export default {
   
   
  vueCustomBlockTransforms: {
   
   
    i18n: ({
   
    code }) => {
   
   
      // i18n用于国际化翻译    
      // return transformed code
    }
  }
}

出于模块化的思想,我们将每个组件增加一个自定义块来存放属性的描述内容,并利用vueCustomBlockTransforms自定义块添加一系列的转换函数。

vue3.0中compiler-core

下面是我在一个博客上找到的图,他对compiler-core的工作流程概括的比较完整。compiler-core 负责「Vue3」中核心编译相关的能力,这包括解析(parse)模板、转化 AST 抽象语法树(transform)、代码生成(generate)等三个过程,它们之间的工作流如下图所示:
在这里插入图片描述
而在kaiteUI这个项目中我们只用到了baseparse这个功能函数,也就是将我们的组件模板渲染成了一个AST抽象树
然后笔者对AST树数据结构进行分析,进入 compiler-core 目录下,结构一目了然。这里说下 _tests_ 目录,是vuejest测试用例。
阅读源码前先看看用例,对阅读源码有很大帮助哦。



如下,测试一个简单的text,执行parse方法之后,得到 ast,期望 ast 的第一个节点与定义的对象是一致的。
同理其他的模块测试用例,在阅读源码前可以先瞄一眼,知道这个模块如何使用,输入输出是啥。

当读取到是纯文本结点时得到的AST抽象树的数据结构
test('simple text', () => {
   
   
    const ast = parse('some text')
    const text = ast.children[0] as TextNode
    expect(text).toStrictEqual({
   
   
        type: NodeTypes.TEXT,
        content: 'some text',
        isEmpty: false,
        loc: {
   
   
            start: {
   
    offset: 0, line: 1, column: 1 },
            end: {
   
    offset: 9, line: 1, column: 10 },
            source: 'some text'
        }
    })
})
当读取到是HTML元素结点时得到的AST抽象树的数据结构
test('simple div', () => {
   
   
      const ast = baseParse('<div>hello</div>')
      const element = ast.children[0] as ElementNode

      expect(element).toStrictEqual({
   
   
        type: NodeTypes.ELEMENT,
        ns: Namespaces.HTML,
        tag: 'div',
        tagType: ElementTypes.ELEMENT,
        codegenNode: undefined,
        props: [],
        isSelfClosing: false,
        children: [
          {
   
   
            type: NodeTypes.TEXT,
            content: 'hello',
            loc: {
   
   
              start: {
   
    offset: 5, line: 1, column: 6 },
              end: {
   
    offset: 10, line: 1, column: 11 },
              source: 'hello'
            }
          }
        ],
        loc: {
   
   
          start: {
   
    offset: 0, line: 1, column: 1 },
          end: {
   
    offset: 16, line: 1, column: 17 },
          source: '<div>hello</div>'
        }
      })
    })

由此我们可以得出contentloc.source是我们想要的关键属性,在源码中笔者找到相关属性存储内容的区别,当读取到文本结点时,会出现content属性,content存放的是文本内容,而当读取到HTML结点时loc.source是包含html标签的内容。

prismjs语法高亮

PrismJS 是一款轻量级、可扩展的语法高亮显示工具,在支持现代 Web 标准基础下增加了更多可选的风格插件。
PrismJS持自定义扩展代码的语言、主题和插件选项,勾选自己常用的代码语言和主题风格以及增强插件,将定制好的代码文件 prism.cssprism.js 如下方式链接到页面,再使用 <pre> <code> 编辑方式编写代码文章即可展现漂亮的代码高亮。

代码实现

组件使用示例模块

此处以Switch组件为例,其中为自定义块

<!-- 自定义块 -->
<demo>
支持 disabled
</demo>

<template>
  <Switch v-model:value="bool" disabled />
</template>

<script lang="ts">
//导入组件
import Switch from "../../../lib/Switch.vue";
//导入ref
import {
    
     ref } from "vue";
export default {
    
    
  components: {
    
    
    Switch,
  },
  setup() {
    
    
  //实现动态绑定
    const bool = ref(false);
    return {
    
    
      bool,
    };
  },
};
</script>
Vue SFC中的自定义块解析模块
这里还有一个插件—将md文件渲染成js文件到页面中,此处不做详细介绍,后面我会单独拿一篇博客来讲解其技术实现
// @ts-nocheck

import {
   
    md } from "./plugins/md";
import fs from "fs";
import {
   
    baseParse } from "@vue/compiler-core";

export default {
   
   
  base: "./",
  assetsDir: "assets",
  //此处模块功能——将md文件渲染到页面中,将单独介绍其技术实现
  plugins: [md()],
  //通过 vueCustomBlockTransforms 选项来指定自定义区块的转换规则
  vueCustomBlockTransforms: {
   
   
    demo: (options) => {
   
   
    //将options解构赋值
      const {
   
    code, path } = options;
      //异步读取文件内容,并转为string类型
      const file = fs.readFileSync(path).toString();
      //将读取到的文件中的自定义快渲染为AST
      const parsed = baseParse(file).children.find((n) => n.tag === "demo");
      //读取自定义模块中的文本内容
      const title = parsed.children[0].content;
   		//将读取文件中的自定义块切分,并转为字符串类型
      const main = file.split(parsed.loc.source).join("").trim();
      //以JSON数据类型返回
      return `export default function (Component) {
        Component.__sourceCode = ${
     
     JSON.stringify(main)}
        Component.__sourceCodeTitle = ${
     
     JSON.stringify(title)}
      }`.trim();
    },
  },
};

我们添加了一个 vueCustomBlockTransforms 对象, 该对象将键 demo(我们的块名)映射到返回虚拟 js 文件的函数中。

vueCustomBlockTransforms 中添加额外字段 __sourceCode__sourceCodeTitle, 该字段映射到我们在任何自定义 <demo> 块中声明的代码。

然后,我们在路由生成代码<h2>{ { component.__sourceCodeTitle }}</h2>中使用此代码。

代码渲染模块
<template>
  <div class="demo">
  <!-- 渲染属性文本内容 -->
    <h2>{
  
  { component.__sourceCodeTitle }}</h2>
    <!-- 功能展示 -->
    <div class="demo-component">
     <!-- vue3.0中绑定组件的指令 -->
      <component :is="component" />
    </div>
    <div class="demo-actions">
      <Button @click="toggleCode" v-if="codeVisible">隐藏代码</Button>
      <Button @click="toggleCode" v-else>查看代码</Button>
    </div>
	<!-- 渲染代码 -->
    <div :class="'demo-code' + [codeVisible ? ' code-show ' : ' code-hidden ']">
      <pre class="language-html" v-html="html" />
    </div>
  </div>
</template>

<script lang="ts">
import Button from "../lib/Button.vue";

import "prismjs";
import "prismjs/themes/prism.css";
import {
    
     computed, ref } from "vue";
const Prism = (window as any).Prism;
export default {
    
    
  components: {
    
    
    Button,
  },
  props: {
    
    
    component: Object,
  },
  setup(props) {
    
    
    const html = computed(() => {
    
    
    //代码高亮
      return Prism.highlight(
        props.component.__sourceCode,
        Prism.languages.html,
        "html"
      );
    });
    const toggleCode = () => (codeVisible.value = !codeVisible.value);
    const codeVisible = ref(false);
    return {
    
    
      Prism,
      html,
      codeVisible,
      toggleCode,
    };
  },
};
</script>

成果展示

kaite-ui官网页面kaite-ui官网页面
github开源地址github开源地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章