作者:夕水
查看效果
作为一个前端开发者,应该基本都会用 VSCode 来做开发,所以也应该见过如下这张图的效果:
以上悬浮面板分为2个部分展示内容。
<element class="hljs-attr">
: 代表元素只有一个类名叫hljs-attr的类选择器,如果有id,比如#app .test
那么这里的展示将变成<element id="app" class="test">
。- 选择器特定性和(0,1,0): 前者是一个链接,可以跳转到mdn,后者分为3个部分,第一个部分代表id选择器的数量,第二个部分代表类选择器的数量,第三个部分代表标签选择器的数量。因此(0,1,0)就代表只有一个类选择器,如果是(1,1,1)代表id,类,标签选择器都有1个,即类似
#app .test div
这样的选择器。
介绍完了以上的功能,接下来,我们就来实现这样一个小组件,不过与原版有所区别的是,我们的实现没有考虑到:hover等伪类或者:first-letter之类的伪元素选择器,我们只做了id选择器,类选择器以及标签选择器,属性选择器的功能也与原版有所差异,想要实现完整的功能,还需要在此基础上进行扩展。
还有一点就是我们增加了总权重的展示。
接下来,我们来看一下我们的最终效果,如下图所示:
创建项目
第一步,先前往 豆包MarsCode 在线 IDE 编辑器地址
ps: 这里需要登陆,自行用各自掘金账号登陆即可。
第二步,选择创建一个项目,如下图所示:
在弹出的面板中选择从模板创建,并选择react。如下图所示:
创建好之后,会为我们生成一个代码地址,并直接跳转,现在你可能会看到如下图所示的目录结构:
它也会为我们自动安装依赖并运行代码。
根据效果图,我们可以知道我们需要用到代码高亮插件,这里的代码高亮插件我选择的是 prismjs,读者也可以自行选择使用代码高亮插件,例如: highlight.js等。
尽管我们也可以自行实现一个代码高亮插件,不过这可以当作另一篇文章的主题了,这里就不自行实现了。
因此,我们需要新开一个终端,或者停止当前终端,来安装代码高亮插件。如下图所示:
使用如下命令安装依赖:
然后我们需要调整一下项目目录结构,最终的目录应该如下图所示:
下面一一说明目录及文件结构:
- utils.ts: 存放工具函数。
- hooks.tsx: 存放钩子函数。
- test.ts: 默认测试的样式代码字符串。
- const.ts: 一些常量。
- components: 存放一些封装好的组件。
代码实现
前期项目准备工作已完成,接下来就进入我们的编码时刻。
代码高亮组件
让我们来分析一下,首先我们需要基于prism.js封装一下代码高亮插件,在components目录下新建一个HighLightCode.tsx。
根据prism.js的文档描述,我们应该知道它是如下这样使用的:
其中第一个参数就是要高亮的代码字符串,第二个参数是导入的语言包,从Prism.languages下取,第三个参数就是我们定义的语言字符串,如: 'html'和'css',然后它的返回值就是经过处理的高亮代码字符串。
当然这里我们只需要用到这2种语言。现在这个代码高亮插件我们就只需要定义2个props即可,如下所示:
也许有人好奇React.HTMLAttributes<HTMLPreElement>
,这里,我们会使用pre和code标签来展示代码,最外层是一个父组件,因此我们需要继承pre标签本身有的一些属性,例如: onClick事件,又或者是其它的一些html属性,所以这里才会继承这个接口。
现在这个组件的结构应该是这样的:
接下来,我们主要是用useMemo来缓存获取高亮后的代码,并使用dangerouslySetInnerHTML属性绑定到code标签中即可,最后我们在父组件使用的时候,还需要访问pre DOM元素,因此我们需要使用ref属性配合ForwardedRef方法使用来完善这个组件。
说明: 如果对ref语法不熟悉,可以查看这篇文章 深入浅出React中的refs。
也许有人会好奇这里为什么要访问DOM元素,这个我们可以放在后面来说明,接下来,我们还是来看看我们完善后的组件。
超链接组件
由于用到了超链接,因此我们稍微基于a标签改造一个Link组件,当然也可以不改造,这取决于你自己。在const.ts中,我们也定义了mdn的跳转链接。如下:
接下来我们来看Link组件。如下所示:
其实说白了,我们主要是增加了rel属性和target属性的默认值吗,其余都是由使用者来自行定义。
工具提示组件
接下来,我们需要实现我们的工具提示组件,这里我们需要访问到pre标签里面的具体的选择器元素。由于代码高亮插件为我们进行了选择器匹配,如下图所示:
因此,我们只需要把类名为selector的元素收集起来,然后监听每个元素的悬浮事件即可,这里由于要收集子元素,因此我们就需要通过ref来访问父元素pre标签元素,这也是前面提到要用ForwardedRef包裹代码高亮的原因。
我们可以获取到这个元素的偏移量,并基于这个偏移量来设置工具提示的偏移量,从而达到悬浮到选择器上就可以在对应的位置出现工具提示的功能。
ps: 当然这里我们的实现还是不完善的,更完善的有现成的插件来实现,例如 popper.js
现在,我们先来看看我们的悬浮提示组件,我们是将悬浮提示的元素添加到body元素中的,因此我们需要使用 createPortal api。
现在这个工具提示组件,我们只需要3个属性,如下:
- visible: 控制工具提示是否渲染。
- children: 渲染子节点,应该是一个react node。
- style: 样式设置,主要用来设置偏移量。
基于以上的分析,我们的tootip组件结构如下:
可以看到这个组件代码结构很简单,然后就是我们的样式代码:
稍微加点对话框的背景和边框色,也加了一个三角形,形成了如前面图中所看到的那样的一个对话框提示。
接下来,我们还要在这个组件的基础上去完善css权重工具提示的组件,但在这之前,我们有必要做一件事,让我们继续往下看。
封装一个解析css选择器字符串的hooks
接下来的这个 Hook 可以用于解析 CSS 选择器字符串并生成对应的 HTML 结构,例如将 #id.class [attr]
转换为 <element id="id" class="class" attr="attr">
。
我们应该如何实现这个解析器呢?首先我们就需要分析css选择器的特性了。我们以一个示例来说明,如下所示:
以上的css选择器,#root是一个id选择器,.app是一个类选择器,以此类推,我们可以看到多个css选择器都是由固定的符号来区分的,如果存在空白,或者","又或者是">",那么前后一定是拆分成2个css选择器的,我们需要根据这些符号将选择器拆分出来,组成一个css选择器数组,不过这里为了统一拆分,我们需要去匹配字符串,转换成统一的分隔符,我这里取的是"s-"。
根据以上分析,我们可以写出如下代码:
还没有结束,假如我们碰到的是这样的css选择器呢?
因此,我们在转换后还需要过滤一下空白,再根据','来分隔一次。代码如下:
接下来我们就根据"s-"来拆分成字符串选择器数组,然后我们依次遍历数组元素,对每一个选择器字符串做解析处理。
对于我们的id选择器,它的第一个字符一定是"#",依次类推,类选择器是".",属性选择器是"[",不过别忘了我们的通配符选择器"*",当然这里为了简便化,暂时不考虑":"也就是伪类选择器和伪元素选择器的情况。
接下来我们就依据第一个字符串来拆分判断,就可以解析出结果来了,不过别忘了效果里面的(0,0,0)的展示,因此这里我们最终的结果需要返回一个对象,它的结构应该是如下这样:
其中id代表统计的id选择器的数量,用作括号里的第一个值展示,依次类推。
有了如上的分析,我们就可以完善我们的解析钩子函数了,如下所示:
这里虽然考虑了属性选择器,但是属性选择器的解析展示是还要继续进行完善的,不过这里就暂时这样,然后再次说明,我们没有考虑伪类选择器和伪元素选择器的情况,如果要考虑,还要再增加一个判断分支,而且每个选择器里面也需要去进行判断,这种情况是比较复杂的。例如考虑一下如下的选择器:
这些场景是很多的,我们要考虑完善的话,那就要增加很多逻辑。好了废话不多说,让我们继续往下看。
最后的对话框提示组件
有了前面所说的钩子函数,我们的代码对话框展示组件实现起来就简单多了。
接下来的代码对话框提示组件,我们所需要做的无非就是将前面的所有代码合并起来使用,代码如下:
App组件
在App组件,我们还需要做一些工作,我们需要一个HighLightCode(高亮代码)组件,用于展示高亮的代码,一个CodeTooltip组件,用于展示悬浮的对话框。
我们给高亮代码组件绑定一个ref,然后收集选择器子元素,并具体给每一个子元素监听悬浮事件,这里为了防止频繁监听,我们还需要使用防抖函数。
然后我们监听悬浮事件,存储当前这个选择器子元素所占据的偏移量,我们需要依据这个偏移量去计算对话框的偏移位置。
然后,对话框的显示与隐藏条件呢?这很容易,我们只要根据这个偏移量来判断即可,怎么判断呢?
我们的偏移量应该是如下值:
很显然如果是一个空对象,那么我们就不展示对话框,否则就展示。
还有,这里我们还需要存储选择器的内容,为什么呢?因为我们需要基于这个选择器的内容进行解析,并渲染到对话框中。即:
根据以上分析,我们可以写出如下代码:
以下是我写的一个简单的样式代码:
接下来,我们需要根据codeRef来收集每一个选择器元素,然后添加悬浮事件的监听,如果鼠标悬浮上去,我们就将当前选择器元素的偏移量和内容存到状态中,注意这里的偏移量,我们是控制了边界值的。理论上我们的左偏移值left应该是当前元素的左偏移值加上它的宽度,再给一个固定的间距值,而顶部偏移量top则直接用当前元素的top减去高度即可。如下:
这里,我们还做了一个判断,就是position中的left和top不相等,也就是不是同一个位置,我们才存储,选择器内容同理。
以上还涉及到了2个工具函数,如何判断一个对象是否为空,以及我们说的防抖函数。这2个工具函数很常用,原理实现也很简单,这里就不做过多说明了,网上也有很多教程说明。我们直接看代码即可:
你以为到了这里就完了吗?不还有最后一步,也就是我们的鼠标如果移出到代码高亮区域之外,我们的对话框则应该需要隐藏,这里还不包括悬浮到对话框区域中。
由于我们的对话框是额外添加的dom,因此我们需要定义一个状态,用来确定鼠标是否悬浮到对话框区域中。
这很简单,CodeTooltip监听mouseenter和mouseleave然后分别修改状态即可。如下:
最后,我们只需要监听代码高亮组件的鼠标移出事件,然后重置position值即可。如下:
想要查看完整示例源码的可以前往 查看: 示例源码。