关于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
核心作用是将字符串转换成 抽象对象语法树ASTjson
是一种轻量级的数据交换格式prismjs
一款轻量级、可扩展的语法高亮显示工具
需求分析
在思考需求分析之前我们先来看看kaiteUI官网的效果演示:
由上面的图我们可以分析出,kaiteUI官网主要又以下几个部分组成:
- 属性功能概述(title)
主要展示组件当前属性的属性关键字 - 组件展示部分(componentShow)
负责将指定属性的组件展示,并且让用户能够与其进行交互。 - 代码展示模块(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_ 目录,是vue
的jest
测试用例。
阅读源码前先看看用例,对阅读源码有很大帮助哦。
如下,测试一个简单的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>'
}
})
})
由此我们可以得出content
和loc.source
是我们想要的关键属性,在源码中笔者找到相关属性存储内容的区别,当读取到文本结点时,会出现content
属性,content
存放的是文本内容,而当读取到HTML结点时loc.source
是包含html标签的内容。
prismjs语法高亮
PrismJS
是一款轻量级、可扩展的语法高亮显示工具,在支持现代 Web 标准基础下增加了更多可选的风格插件。
PrismJS
支持自定义扩展代码的语言、主题和插件选项,勾选自己常用的代码语言和主题风格以及增强插件,将定制好的代码文件 prism.css
和 prism.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文件到页面中,此处不做详细介绍,后面我会单独拿一篇博客来讲解其技术实现【vite+vue3.0】基于vite写一个将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开源地址