需求背景
事情是这样的,后端返回类似这样的数据格式 cshjdkvghsv<xxx xx='xxx' />sdhjkshfv'<xxx xx='xxx'></xxx>'sdhgjdsk
,然后需要前端将字符串中的xxx
解析出来并渲染到页面中,还要保证渲染顺序,其中xxx可能是一个自定义的react组件名,也有可能是一个html元素标签名,而xx则可能是属性名,’xxx’则是属性值,比如className=’base-1’。比如xxx是div元素,我们最终的页面渲染结果就应该是。
1 | html复制代码cshjdkvghsv<div className='test-1'></div>sdhjkshfv'<div className='test-2'></div>'sdhgjdsk |
如果只是单纯的html元素,其实我们只需要使用dangerousSetInnerHTML属性即可,可是这里还有自定义组件元素呀。
最终的结果就是,我们需要对模板字符串进行解析,解析成一个数组,方便渲染,最终我们的react组件使用如下:
1 | tsx复制代码import React, { Fragment, createElement } from 'react'; |
然后就可以得到我们实际想要渲染的效果,那么现在的问题就是,如何将模板字符串渲染成一个顺序正常的数组。
即实现如下效果:
1 | js复制代码const str = '111<CustomComponent value="123">222'; |
这里,我们就需要用到正则表达式,当然由于正则表达式的实现不同,可能会存在限制。
实现思路
我们实现这个解析器的思路很简单,我们会用一个结果数组来存储最终的结果,这个数组的数组项可能是字符串,也有可能是react组件对象,即{type: string,props:Record<string,string>}
,因此我们是选需要定义一个类型,如下所示:
1 | ts复制代码type ReactComponentObj = { type: string; props: Record<string, string> }; |
ps:其实严格来说props的属性值不一定是字符串,不过这里我们是根据需求来扩展的,我们先实现一个基础版本。
为了提取出props和组件元素名,我们需要定义3个正则表达式,如下所示:
1 | ts复制代码// 匹配组件元素 |
以上正则表达式也有可能存在问题,不过还是可以满足当下的需求。
首先我们还是需要定义一个函数以及一些变量,如下所示:
1 | ts复制代码const simpleJSXParser = (template: string) => { |
接下来就是核心逻辑,我们可以如此实现,我们需要定义2个变量,一个用来拼接普通内容字符串,一个用来拼接匹配到的元素字符串,然后我们根据字符长度去循环,每次匹配到一个拼接的字符串,就将总字符串长度减去拼接的字符串的长度,这也是结束循环所必须要做的。代码如下所示:
1 | ts复制代码const simpleJSXParser = (template: string) => { |
如果匹配到组件元素字符串,我们则需要单独进行处理,因此我们实现一个createComponent方法,该方法的参数就是组件元素字符串,如下所示:
1 | ts复制代码const simpleJSXParser = (template: string) => { |
可以看到,这个方法我们会返回一个组件对象,这将在后面介绍实现原理,这里我们先跳过,接下来我们来看循环里面的逻辑实现。
在循环里面,我们会先判断是否匹配到组件元素,如果没有,则整个模板字符串就是普通的字符串内容,直接添加到结果数组中即可。
如果含有组件元素,则获取匹配到的组件元素的起始索引值,通过字符串的match方法即可,当match方法匹配到对应的字符串,则会有对应的index属性,这就是匹配的索引值。
然后我们从0开始到起始索引值为止,拼接每一个字符,因为组件元素字符串前面可能会存在普通内容字符串,因此我们需要循环拼接这个字符串。
拼接完成之后,我们要添加到结果数组当中,然后从模板字符串中剔除掉resStr字符,使用replace方法匹配resStr即可,并修改外层循环i的索引值,即i -= resStr.length
,修改完成之后,我们还要重置该结果值,即resStr = ''
。
根据如上分析,我们可以写出如下代码:
1 | ts复制代码const simpleJSXParser = (template: string) => { |
接下来剩余字符串还会有组件元素和普通字符串的情况,我们继续匹配组件元素,按照同样的思路,拼接组件元素字符串到另一个变量,并添加到结果数组中,注意这个时候,我们就需要调用createComponent方法,将组件元素字符串转换成对应的组件数据对象。如下所示:
1 | ts复制代码const simpleJSXParser = (template: string) => { |
接下来,我们就来看createComponent的实现,这个函数的作用其实就是从中找出组件名,以及相应的属性,然后返回即可。同样我们还是利用正则表达式,这里也涉及到了一个正则表达式捕获组的概念。
ps: 关于正则表达式捕获组的概念可以参考这篇文章。
通常我们的组件元素都是英文字母,因此我们可以通过/\w/
来匹配组件名,而属性名和属性值,我们则可以通过componentPropRegExp来匹配,可以发现我们创建了2个捕获组,第一个捕获组和第二个捕获组分别就是我们想要的属性名和属性值,因此我们只需要提取属性名和属性值组合成对象即可。
根据以上分析,我们就可以写出如下代码:
1 | ts复制代码const simpleJSXParser = (template: string) => { |
将以上代码整合起来就得到了我们最终解析器的代码,如下所示:
1 | ts复制代码type ReactComponentObj = { type: string; props: Record<string, string> }; |
当然,以上代码还存在不少问题,首先组件元素名我们也会有存在"-"
的情况,这里我们并没有考虑进去,其次匹配标签的时候,这里的正则表达式是忽略掉了单闭合标签的,只能匹配成对的标签,这也是后续需要考虑优化的事情。
接下来,我们来使用一下这个解析器。
应用
使用vite初始化一个react-ts项目,然后新建一个utils目录,创建data.ts,代码如下所示:
1 | ts复制代码import { createUUID } from "./uuid"; |
嗯,这里涉及到了一个uuid方法,代码如下:
1 | ts复制代码export const createUUID = () => (Math.random() * 10000000).toString(16).substr(0, 4) + '-' + (new Date()).getTime() + '-' + Math.random().toString().substr(2, 5); |
接下来新建一个components目录,实现我们的2个组件CustomInput与CustomTag并写上一些样式,然后在app.tsx里面,我们就可以使用了,我们会使用createElement方法,如下所示:
1 | ts复制代码import { createUUID } from './utils/uuid'; |
最终会得到如下图所示的渲染效果:
感谢大家阅读本文,觉得本文不错,希望不吝啬点赞收藏。
本文转载自: 掘金