我用豆包MarsCode IDE 做了一个 CSS 权重小组件

embedded/2024/11/23 6:41:23/
htmledit_views">

作者:夕水

查看效果

作为一个html" title=前端>前端开发者,应该基本都会用VSCode来做开发,所以也应该见过如下这张图的效果:

以上悬浮面板分为2个部分展示内容。

  1. <element class="hljs-attr">: 代表元素只有一个类名叫hljs-attr的类选择器,如果有id,比如#app .test那么这里的展示将变成<element id="app" class="test">
  2. 选择器特定性和(0,1,0): 前者是一个链接,可以跳转到mdn,后者分为3个部分,第一个部分代表id选择器的数量,第二个部分代表类选择器的数量,第三个部分代表标签选择器的数量。因此(0,1,0)就代表只有一个类选择器,如果是(1,1,1)代表id,类,标签选择器都有1个,即类似#app .test div这样的选择器。

介绍完了以上的功能,接下来,我们就来实现这样一个小组件,不过与原版有所区别的是,我们的实现没有考虑到:hover等伪类或者:first-letter之类的伪元素选择器,我们只做了id选择器,类选择器以及标签选择器,属性选择器的功能也与原版有所差异,想要实现完整的功能,还需要在此基础上进行扩展。

还有一点就是我们增加了总权重的展示。

接下来,我们来看一下我们的最终效果,如下图所示:

创建项目

第一步,先前往豆包MarsCode在线IDE编辑器地址 www.marscode.cn/dashboard 。

ps: 这里需要登陆,自行用各自掘金账号登陆即可。

第二步,选择创建一个项目,如下图所示:

在弹出的面板中选择从模板创建,并选择react。如下图所示:

创建好之后,会为我们生成一个代码地址,并直接跳转,现在你可能会看到如下图所示的目录结构:

它也会为我们自动安装依赖并运行代码。

根据效果图,我们可以知道我们需要用到代码高亮插件,这里的代码高亮插件我选择的是prismjs,读者也可以自行选择使用代码高亮插件,例如: highlight.js等。

尽管我们也可以自行实现一个代码高亮插件,不过这可以当作另一篇文章的主题了,这里就不自行实现了。

因此,我们需要新开一个终端,或者停止当前终端,来安装代码高亮插件。如下图所示:

使用如下命令安装依赖:

pnpm add prismjs @types/prismjs

 然后我们需要调整一下项目目录结构,最终的目录应该如下图所示:

下面一一说明目录及文件结构:

  1. utils.ts: 存放工具函数。
  2. hooks.tsx: 存放钩子函数。
  3. test.ts: 默认测试的样式代码字符串。
  4. const.ts: 一些常量。
  5. components: 存放一些封装好的组件。

代码实现

前期项目准备工作已完成,接下来就进入我们的编码时刻。

代码高亮组件

让我们来分析一下,首先我们需要基于prism.js封装一下代码高亮插件,在components目录下新建一个HighLightCode.tsx。

根据prism.js的文档描述,我们应该知道它是如下这样使用的:

const hignlightCode = Prism.highligh(code, lang, lang);

其中第一个参数就是要高亮的代码字符串,第二个参数是导入的语言包,从Prism.languages下取,第三个参数就是我们定义的语言字符串,如: 'html'和'html" title=css>css',然后它的返回值就是经过处理的高亮代码字符串。

当然这里我们只需要用到这2种语言。现在这个代码高亮插件我们就只需要定义2个props即可,如下所示:

export interface HighLightCodeProps extends React.HTMLAttributes<HTMLPreElement>{code?: string;language?: keyof Prism.Languages & string;
}

也许有人好奇React.HTMLAttributes<HTMLPreElement>,这里,我们会使用pre和code标签来展示代码,最外层是一个父组件,因此我们需要继承pre标签本身有的一些属性,例如: onClick事件,又或者是其它的一些html属性,所以这里才会继承这个接口。

现在这个组件的结构应该是这样的:

import Prism from 'prismjs';
import 'prismjs/themes/prism.html" title=css>css';
export interface HighLightCodeProps extends React.HTMLAttributes<HTMLPreElement>{code?: string;language?: keyof Prism.Languages & string;
}const HighLightCode = ({ code, language = 'html" title=css>css',...rest }: HighLightCodeProps) => {// ...return (<><pre className='pre' {...rest }><code {/*...*/}/></pre></>)
}export default HighLightCode

 接下来,我们主要是用useMemo来缓存获取高亮后的代码,并使用dangerouslySetInnerHTML属性绑定到code标签中即可,最后我们在父组件使用的时候,还需要访问pre DOM元素,因此我们需要使用ref属性配合ForwardedRef方法使用来完善这个组件。

说明: 如果对ref语法不熟悉,可以查看这篇文章深入浅出React中的refs。

也许有人会好奇这里为什么要访问DOM元素,这个我们可以放在后面来说明,接下来,我们还是来看看我们完善后的组件。

// ...
import { ForwardedRef, forwardRef, useMemo } from 'react';
// ...const HighLightCode = forwardRef(({ code, language = 'html" title=css>css',...rest }: HighLightCodeProps, ref: ForwardedRef<HTMLPreElement>) => {// 这里相当于监听code是否变化,如果未变化,将采取缓存值const hightLightCode = useMemo(() => {if (code) {return Prism.highlight(code, Prism.languages[language], language);}return '';}, [code])return (<><pre className='pre' ref={ref} {...rest }><code dangerouslySetInnerHTML={{ __html: hightLightCode }} /></pre></>)
})export default HighLightCode

超链接组件

由于用到了超链接,因此我们稍微基于a标签改造一个Link组件,当然也可以不改造,这取决于你自己。在const.ts中,我们也定义了mdn的跳转链接。如下:

// const.ts中
export const MDN_LINK = "https://developer.mozilla.org/docs/Web/CSS/Specificity"

接下来我们来看Link组件。如下所示:

import { AnchorHTMLAttributes } from "react";export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {children?: React.ReactNode;
}const Link = ({ children,rel = 'noopener noreferrer',target = '_blank',...rest }: LinkProps) => {const attrMap = {...rest,rel,target}return (<a {...attrMap}>{children}</a>)
}export default Link;

其实说白了,我们主要是增加了rel属性和target属性的默认值吗,其余都是由使用者来自行定义。

工具提示组件

接下来,我们需要实现我们的工具提示组件,这里我们需要访问到pre标签里面的具体的选择器元素。由于代码高亮插件为我们进行了选择器匹配,如下图所示:

因此,我们只需要把类名为selector的元素收集起来,然后监听每个元素的悬浮事件即可,这里由于要收集子元素,因此我们就需要通过ref来访问父元素pre标签元素,这也是前面提到要用ForwardedRef包裹代码高亮的原因。

我们可以获取到这个元素的偏移量,并基于这个偏移量来设置工具提示的偏移量,从而达到悬浮到选择器上就可以在对应的位置出现工具提示的功能。

ps: 当然这里我们的实现还是不完善的,更完善的有现成的插件来实现,例如popper.js

现在,我们先来看看我们的悬浮提示组件,我们是将悬浮提示的元素添加到body元素中的,因此我们需要使用createPortal api。

现在这个工具提示组件,我们只需要3个属性,如下:

  1. visible: 控制工具提示是否渲染。
  2. children: 渲染子节点,应该是一个react node。
  3. style: 样式设置,主要用来设置偏移量。

基于以上的分析,我们的tootip组件结构如下:

import { CSSProperties, useId, useMemo } from "react"
import { createPortal } from "react-dom";
export interface TooltipProps extends React.HTMLAttributes<HTMLDivElement>{ visible?: boolean;children?: React.ReactNode;style?: CSSProperties;
}
const Tooltip = ({ visible,children,style,...rest }: TooltipProps) => {const toolTipId = useId();return (<>{visible && createPortal(<div id={toolTipId} className="tooltip" style={style} {...rest}>{children}</div>,document.body)}</>)
}export default Tooltip;

可以看到这个组件代码结构很简单,然后就是我们的样式代码:

.tooltip {position: fixed;padding: 8px 12px;border: 1px solid #dcdcdc;background-color: #f5f4f4;color: #979698;border-radius: 8px;box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.1);min-width: 150px;min-height: 60px;
}.tooltip::before {content: "";width: 0;height: 0;border-style: solid;border-width: 10px;border-color: transparent transparent #f5f4f4 transparent;position: absolute;left: -20px;top: 10px;transform: rotate(-90deg);
}.tooltip a {margin-right: 5px;
}

稍微加点对话框的背景和边框色,也加了一个三角形,形成了如前面图中所看到的那样的一个对话框提示。

接下来,我们还要在这个组件的基础上去完善html" title=css>css权重工具提示的组件,但在这之前,我们有必要做一件事,让我们继续往下看。

封装一个解析html" title=css>css选择器字符串的hooks

接下来的这个 Hook 可以用于解析 CSS 选择器字符串并生成对应的 HTML 结构,例如将 #id.class [attr] 转换为 <element id="id" class="class" attr="attr">

我们应该如何实现这个解析器呢?首先我们就需要分析html" title=css>css选择器的特性了。我们以一个示例来说明,如下所示:

#root .app > span + .hover ~ .text, .active {// 样式代码
}

以上的html" title=css>css选择器,#root是一个id选择器,.app是一个类选择器,以此类推,我们可以看到多个html" title=css>css选择器都是由固定的符号来区分的,如果存在空白,或者","又或者是">",那么前后一定是拆分成2个html" title=css>css选择器的,我们需要根据这些符号将选择器拆分出来,组成一个html" title=css>css选择器数组,不过这里为了统一拆分,我们需要去匹配字符串,转换成统一的分隔符,我这里取的是"s-"。

根据以上分析,我们可以写出如下代码:

export const useCssTypeCode = (str: string) => {const splitSymbol = [' ', '>', '+', '~', ',']splitSymbol.forEach((symbol) => {// 将匹配到的符号转换成统一的s-分隔符str = str.replace(symbol, 's-');});// ...
}

还没有结束,假如我们碰到的是这样的html" title=css>css选择器呢?

#app,
.app,
.text {// 样式代码
}

因此,我们在转换后还需要过滤一下空白,再根据','来分隔一次。代码如下:

export const useCssTypeCode = (str: string) => {const splitSymbol = [' ', '>', '+', '~', ',']splitSymbol.forEach((symbol) => {// 将匹配到的符号转换成统一的s-分隔符,然后过滤掉空白,并根据,来继续做拆分str = str.replace(symbol, 's-').replace(/\s/g, '').replace(/,/g, 's-');});// ...
}

接下来我们就根据"s-"来拆分成字符串选择器数组,然后我们依次遍历数组元素,对每一个选择器字符串做解析处理。

对于我们的id选择器,它的第一个字符一定是"#",依次类推,类选择器是".",属性选择器是"[",不过别忘了我们的通配符选择器"*",当然这里为了简便化,暂时不考虑":"也就是伪类选择器和伪元素选择器的情况。

接下来我们就依据第一个字符串来拆分判断,就可以解析出结果来了,不过别忘了效果里面的(0,0,0)的展示,因此这里我们最终的结果需要返回一个对象,它的结构应该是如下这样:

{ res: '', id: 0, className: 0, tag: 0 }

其中id代表统计的id选择器的数量,用作括号里的第一个值展示,依次类推。

有了如上的分析,我们就可以完善我们的解析钩子函数了,如下所示:

export const useCssTypeCode = (str: string) => {const splitSymbol = [' ', '>', '+', '~', ',']splitSymbol.forEach((symbol) => {str = str.replace(symbol, 's-').replace(/\s/g, '').replace(/,/g, 's-');});return str.split('s-').reduce((res, item) => {if (item[0] === '#') {res.res += `<element id="${item.slice(1)}">`;res.id += 1;} else if (item[0] === '.') {res.className += 1;res.res += `<element className="${item.slice(1)}">`;} else if (item[0] === '[') {res.className += 1;res.res += `<element attr="${item.slice(1, -1)}">`} else if (item === '*') {res.res += `<element>`} else {res.tag += 1;res.res += `<${item}>`}return res;}, { res: '', id: 0, className: 0, tag: 0 })
}

这里虽然考虑了属性选择器,但是属性选择器的解析展示是还要继续进行完善的,不过这里就暂时这样,然后再次说明,我们没有考虑伪类选择器和伪元素选择器的情况,如果要考虑,还要再增加一个判断分支,而且每个选择器里面也需要去进行判断,这种情况是比较复杂的。例如考虑一下如下的选择器:

.text:hover {}
#app:hover,.test:hover {}
// ...

这些场景是很多的,我们要考虑完善的话,那就要增加很多逻辑。好了废话不多说,让我们继续往下看。

最后的对话框提示组件

有了前面所说的钩子函数,我们的代码对话框展示组件实现起来就简单多了。

接下来的代码对话框提示组件,我们所需要做的无非就是将前面的所有代码合并起来使用,代码如下:

import { CSSProperties } from "react";
import Tooltip from "./Tooltip";
import HighLightCode from "./HighLightCode";
import Link from "./Link";
import { MDN_LINK } from "../const";
import { useCssTypeCode } from "../hooks";export interface CodeTooltipProps extends React.HTMLAttributes<HTMLDivElement>{visible?: boolean;style?: CSSProperties;code: string;
}const CodeTooltip = ({ visible, style,code,...rest }: CodeTooltipProps) => {const { res,id,tag,className } = useCssTypeCode(code);return (<Tooltip visible={visible} style={style} {...rest}>// 对话框里的html代码展示<div className="line-code"><HighLightCode code={res} language="html" /></div>// 链接展示<Link href={MDN_LINK}>选择器特性:</Link>// 权重展示<span>({ id },{ className },{ tag })</span>// 计算总权重<div>总权重为:{ id * 100 + className * 10 + tag }</div></Tooltip>)
}export default CodeTooltip;

App组件

在App组件,我们还需要做一些工作,我们需要一个HighLightCode(高亮代码)组件,用于展示高亮的代码,一个CodeTooltip组件,用于展示悬浮的对话框。

我们给高亮代码组件绑定一个ref,然后收集选择器子元素,并具体给每一个子元素监听悬浮事件,这里为了防止频繁监听,我们还需要使用防抖函数。

然后我们监听悬浮事件,存储当前这个选择器子元素所占据的偏移量,我们需要依据这个偏移量去计算对话框的偏移位置。

然后,对话框的显示与隐藏条件呢?这很容易,我们只要根据这个偏移量来判断即可,怎么判断呢?

我们的偏移量应该是如下值:

 const [position, setPosition] = useState<{ left?: number, top?: number }>({});

根据以上分析,我们可以写出如下代码:

// ...const App = () => {const codeRef = useRef<HTMLPreElement>(null);const [position, setPosition] = useState<{ left?: number, top?: number }>({});const [content, setContent] = useState('');useEffect(() => {if (codeRef.current) {// ....}}, [])const visible = useMemo(() => !isEmptyObject(position), [position])return (<div className="app"><HighLightCode// 测试的样式代码code={testStyle}ref={codeRef}/><CodeTooltipcode={content}visible={visible}style={position}/></div>);
};export default App;

以下是我写的一个简单的样式代码:

export const testStyle = `
* {margin: 0;padding: 0;font-family: JetBrainsMono-Regular, "微软雅黑", sans-serif;box-sizing: border-box;
}#app {width: 100vw;height: 100vh;display: flex;flex-direction: column;justify-content: center;align-items: center;
}#root>div {width: 100%;
}.flex {display: flex;flex-direction: column;justify-content: center;align-items: center;
}body {margin: 0;
}

接下来,我们需要根据codeRef来收集每一个选择器元素,然后添加悬浮事件的监听,如果鼠标悬浮上去,我们就将当前选择器元素的偏移量和内容存到状态中,注意这里的偏移量,我们是控制了边界值的。理论上我们的左偏移值left应该是当前元素的左偏移值加上它的宽度,再给一个固定的间距值,而顶部偏移量top则直接用当前元素的top减去高度即可。如下:

  useEffect(() => {if (codeRef.current) {const selectorElements = codeRef.current.querySelectorAll('span.selector');selectorElements.forEach((el) => {el.addEventListener('mouseenter', debounce(() => {const { left, top, width, height } = el.getBoundingClientRect();const leftValue = left + width + 10,topValue = top - height;if (position.left !== leftValue || position.top !== topValue) {setPosition({ left: Math.min(leftValue, window.innerWidth), top: Math.min(topValue, window.innerHeight) });}if (el.textContent !== content && el.textContent) {setContent(el.textContent);}}, 200))});}}, [])

这里,我们还做了一个判断,就是position中的left和top不相等,也就是不是同一个位置,我们才存储,选择器内容同理。

以上还涉及到了2个工具函数,如何判断一个对象是否为空,以及我们说的防抖函数。这2个工具函数很常用,原理实现也很简单,这里就不做过多说明了,网上也有很多教程说明。我们直接看代码即可:

export const isEmptyObject = (val: unknown) => {if (val === null || val === undefined) return true;if (typeof val !== 'object') return true;if (Array.isArray(val)) return val.length === 0;return Object.keys(val).length === 0;
};export const debounce = <T extends any[]>(handler: (...args: T) => void, ms: number): ((...args: T) => void) => {let time: ReturnType<typeof setTimeout> | null = null;return function fn(this: typeof fn,...args: T) {time && clearTimeout(time);time = setTimeout(() => handler.apply(this, args), ms);};};

你以为到了这里就完了吗?不还有最后一步,也就是我们的鼠标如果移出到代码高亮区域之外,我们的对话框则应该需要隐藏,这里还不包括悬浮到对话框区域中。

由于我们的对话框是额外添加的dom,因此我们需要定义一个状态,用来确定鼠标是否悬浮到对话框区域中。

这很简单,CodeTooltip监听mouseenter和mouseleave然后分别修改状态即可。如下:

// 是否在对话框区域
const [isPanel, setIsPanel] = useState(true);
// CodeTooltip组件中
<CodeTooltip// ...onMouseEnter={() => setIsPanel(true)}onMouseLeave={() => setIsPanel(false)}
/>

最后,我们只需要监听代码高亮组件的鼠标移出事件,然后重置position值即可。如下:

<HighLightCode// ...onMouseLeave={debounce((e) => {if (!isPanel) {setPosition({});setContent('');}}, 200)}
/>

想要查看完整示例源码的可以点击这里查看~


http://www.ppmy.cn/embedded/139264.html

相关文章

无尽梦回公测 低配置手机如何玩无尽梦回?手机发烫内存不足玩无尽梦回攻略

无尽梦回公测将于 11 月 21 日正式开启 &#xff01;多角色动作肉鸽爽游&#xff0c;横扫14亿梦境&#xff01; 无尽梦回是一款以14亿人共通梦境为题材的多角色动作肉鸽爽游。低上手门槛&#xff0c;华丽纯爽感&#xff01;你将在梦境专卖店成为「捕梦者」&#xff0c;进入无秩…

【AI大模型引领变革】探索AI如何重塑软件开发流程与未来趋势

文章目录 每日一句正能量前言流程与模式介绍【传统软件开发 VS AI参与的软件开发】一、传统软件开发流程与模式二、AI参与的软件开发流程与模式三、AI带来的不同之处 结论 AI在软件开发流程中的优势、挑战及应对策略AI在软件开发流程中的优势面临的挑战及应对策略 结论 后记 每…

【汇编语言】转移指令的原理(三) —— 汇编跳转指南:jcxz、loop与位移的深度解读

文章目录 前言1. jcxz 指令1.1 什么是jcxz指令1.2 如何操作 2. loop 指令2.1 什么是loop指令2.2 如何操作 3. 根据位移进行转移的意义3.1 为什么&#xff1f;3.2 举例说明 4. 编译器对转移位移超界的检测结语 前言 &#x1f4cc; 汇编语言是很多相关课程&#xff08;如数据结构…

PW系列工控电脑复制机:效率与精度双重提升

工控电脑复制应用&#xff1a;效率与精度的双重提升 随着现代企业对大数据、数据备份、和跨平台兼容性需求的快速增长&#xff0c;工控电脑已成为数据密集型产业的核心设备。针对工控环境中大量数据复制的特殊需求&#xff0c;PW系列NVMe/SATA PCIe SSD复制机&#xff08;如PW…

怎么做好白盒测试?

白盒测试 一、什么是白盒测试&#xff1f;二、白盒测试特点三、白盒测试的设计方法1、逻辑覆盖法1、测试设计方法—语句覆盖a、用例设计如下&#xff1a;b、语句覆盖的局限性 2、测试设计方法—判定覆盖a、测试用例如下&#xff1a;b、判定覆盖的局限性 3、测试设计方法—条件覆…

IntelliJ IDEA 2024.3 K2 模式已发布稳定版,Android Studio Meerkat 预览也正式支持

关于 K2 Mode 在 Android Studio Ladybug 更新里我们简单介绍过&#xff0c;因为 Ladybug 采用的是 IntelliJ IDEA 2024.2 &#xff0c;所以 AS 也开始支持新的 K2 机制&#xff0c;例如支持 gradle.kts 、KMP 项目、代码突出显示、调试等。 而从 IntelliJ IDEA 2024.3 版本开始…

bpmn.js显示流程图

目标&#xff1a;vue2 接口返回xml - 弹窗显示流程图 - 根据需求高亮节点 一、安装依赖 npm i bpmn-js 添加上的版本是 "bpmn-js": "^11.5.0" 二、只读显示流程图 <div ref"canvas" style"width:100%;height: calc(100vh…

VueDPlayer视频插件的使用

VueDPlayer 是一个基于 DPlayer 的 Vue 封装组件&#xff0c;DPlayer 是一个 HTML5 视频播放器&#xff0c;支持弹幕、视频倍速播放、视频预加载等功能。 以下是如何在 Vue.js 项目中使用 VueDPlayer 的步骤&#xff1a; 1.安装 VueDPlayer&#xff1a; 使用 npm 或 yarn 安装…