手把手教你实现网页端社交应用中的@人功能:技术原理、代码示例等

频道:社交专题 日期: 浏览:42

它是由ELab团队技术团队为大家作的分享,原本的题目是“Twitter还有微博都在运用的 @ 人的功能究竟是怎样进行设计以及实现的?”,现在是有过修订的。

1、引言

目前所使用的@人功能,距离初次使用已过去差不多10年之久,初次使用时是借助微博来体验的,@人功能现今在各类诸多应用中尽数存在,该功能涉及社交(IM、微博)、办公(钉钉、企业微信)等场景,已然成为一个绝对不能缺少的功能。

这段时光刚好处于调研IM各类功能技术实现方案的阶段,因而也针对@人功能于Web网页前端之中的技术实现作了详尽了解,恰好借此契机为诸位呈上我所掌握的技术原理以及代码实现。

学习交流:

移动端IM开发入门的文章是《新手入门一篇就够:从零开发移动端IM》,开源IM框架源码的链接是https://github.com/JackJiang2011/MobileIMSDK,还有相关资料。

本文所分享的@人功能,是面向Web网页前端的。而其跟移动端原生代码的实现,在技术原理方面,以及在实际实现这一方面,存在着相当大的差异。所以,要是想知晓移动端IM这种社交应用里的@人实现功能,那么能够去读一下《Android端IM应用中的@人功能实现:仿微博、QQ、微信,零入侵、高可扩展》。

图文+源码

》这篇文章。

3、业内实现

3.1 微博的实现

微博的达成相对简易 ,乃是借由正则匹配 ,最终以空格表明匹配告终 ,故而在实现层面直接运用了textarea标签。

但是这个实现必须依赖的一个事情是:用户名必须唯一。

微博的用户名具有唯一性,因此正则所匹配到的ID,通常情况下能够映射到唯一的一个用户身上(除非ID不存在)。然而,微博这个功能的整体输出相对宽松,你可以构造任何不存在的ID来进行@操作。

3.2 Twitter的实现

Twitter的达成情形跟微博相类,同样是以@起始,空格收尾来做般配。不过运用的是contenteditable这个特性去开展富文本操控。

相似之处是那样的,Twitter 的 ID 同样是唯一的,然而,能够借助昵称去开展搜索,接着把它转化为 ID,在体验这方面,这可是好了挺多的。

4、技术思路

通过分析业内的主流实现,@人功能的技术实现思路大致如下:

总的来讲,要是如同平常所采用的Lark搜索(Lark即“飞书”),那么我们不会借助唯一的『工号』来展开搜索,而是凭借名字,然则名字会出现重复的情况,从而不太适宜采用textarea的形式,而是运用contenteditable,将@文本置换成HTML标签特殊化标记。

5、代码实现第1步:获得用户的光标位置

想要得到用户输入的字符串,接着替换进去,第一步便是获取用户所在的光标,要获取光标信息,就得先清楚什么是『选择(Selection) 』,以及什么是『范围(Range) 』。

5.1 范围(Range)

Range本质上是一对“边界点”:范围起点和范围终点。

每一个点,皆被显示成有着相对于起始点的相对偏移值的一个父 DOM 节点。要是父节点属于元素节点范畴,如此一来,偏移量便是子节点的编号,可是对于文本节点而言,其偏移量则为文本里的位置。

例如:

let range = newRange();

之后,运用range.setStart(node, offset),而后,运用range.setEnd(node, offset),以此来设定选择边界。

假设 HTML 片段是这样的:

Example: italic and bold

选择 "Example: italic",它是

的前两个子节点(文本节点也算在内):

Example: italic and bold

解释一下:

如果像这样操作:

能够做到这一点,只需把起点以及终点设定成文本节点里的相对偏移量就行。

我们需要创建一个范围:

Example: italic and bold

range 对象具有以下属性:

解释一下:

5.2 选择(Selection)

Range 是用于管理选择范围的通用对象。

文档选择是被 Selection 对象所表示的,能够借助 window.getSelection() 或者 document.getSelection() 去获取。

依据 Selection API 规范,存在这样一种情况,一个选择能够涵盖零个或者多个范围,然而实际上,单单只有 Firefox 具备这样的特性,也就是允许借助 Ctrl+click(在 Mac 系统上则是 Cmd+click)于文档当中选取多个范围。

这是一张截图,是在Firefox里做出来的,是关于一个选择的截图,这个选择具有3个范围。

其他浏览器最多支持 1 个范围。

正如我们将会看到的那样,某些 Selection 方法意味着也许存在多个范围,然而同样地,在除了 Firefox 之外的每一个浏览器当中,范围最多为 1。

同范围相仿,被选的起始点称作“锚点(anchor)”,而终点叫做“焦点(focus)”。

主要的选择属性有:

看完上面,不知道了解了没?没关系,我们继续往下。

综上所讲:通常来讲我们仅仅唯有一个Range,在我们的光标于contenteditable的div之上进行闪动之际,实际上就已然拥有了一个Range,而这个Range的起始位置以及结束位置是完全相同的。

此外,我们能够直接借助 Selection.focusNode 获取那与之对应的节点,依靠 Selection.focusOffset 获取那与之对应的偏移量。

就像下图:

如此一来,咱便得到了,光标的所在位置。并且,还得到了与之相对应的,TextNode对象。

6、代码实现第2步:获取需要@的用户

在上一节,我们得到了光标于对应Node节点处的偏移量,还拥有了对应的Node节点。如此一来,便能借由textContent方法去获得整个文本。

一般来说,通过一个简单的正则就可以获取@的内容了:

// 获取光标位置,为获取光标位置,定义函数 getCursorIndex,该函数进行如下操作:首先获取选框,通过 window.getSelection() 获取选框,然后返回选框的焦点偏移量,即返回 selection?.focusOffset。// 获取节点,为获取节点,定义函数 getRangeNode,该函数执行以下步骤:先获取选择范围,借助 window.getSelection() 获取选择范围,接着返回焦点节点,也就是返回 selection?.focusNode。// 获取 @ 用户,为获取 @ 用户,定义函数 getAtUser,此函数有以下行为:先获取节点文本内容,获取 getRangeNode()?.textContent,若为空则设为空字符串,接着创建正则表达式,创建 /@(*)$/ 的正则表达式,然后进行匹配操作,对 content.slice(0, getCursorIndex()) 进行匹配,若匹配且匹配长度为 2,则返回匹配结果,否则返回 undefined。

之所以要截取光标前的文本,是由于@的插入位置存在两种情况,一种是在末尾,另一种是在中间,而这两种情况都使得在进行判断之前,必须进行截取操作。

所以简单地slice一下就好了:

content.slice(0, getCursorIndex()),第7点,代码实现的第3步是弹窗展示以及按键拦截。

弹窗是否展示的逻辑,跟判断@用户类似,都是同一个正则。

// 是不是要展示 @ ,那这个常量 showAt 是这样定义的,它是一个函数,函数内部,会获取一个范围节点,把它赋值给常量 node ,要是 node 不存在,或者 node 的节点类型不是文本节点,那就返回 false 了,接着,获取 node 的文本内容,要是没有文本内容就赋值为空字符串,给常量 content ,再定义一个正则表达式 regx ,用它去匹配从内容开始到获取光标索引位置的这一段内容,把匹配结果给常量 match ,最后返回 match 存在且 match 的长度是 2 这种情况。

对于弹窗而言,其应当出现在恰当的位置上,还好的是,现代浏览器存在着不少较为好用的API。

常量获取范围矩形函数等于一个箭头函数,这个箭头表示返回的内容,在这个函数内部,首先常量选择等于窗口获取选择,然后常量范围等于选择所获取的范围中的第一个,接着常量矩形等于以上范围获取元素的矩形集合,再接着常量行高等于三十,最后返回一个对象,该对象里有一个新的元素常量x等于上面刚刚说的矩形的x值,还有一个常量y等于上面矩形的y值加上刚才定义的行高调即用常量行高表示的值以上都在一个大对象中。

当弹窗出现之后,我们还要把输入框的『上』、『下』、『回车』操作拦截掉,不然输入框响应这些按键会致使光标位置偏移到别的地方。

定义一个名为handleKeyDown的函数,该函数接收一个参数e,其类型为any,在函数内部,若showDialog为真,当e的code值等于"ArrowUp" 或者e的code值等于"ArrowDown" 或者e的code值等于"Enter"时,函数中执行e.preventDefault() ,标点符号。

然后,于弹窗之中,监听这些按键,达成上下选择、回车确定,关闭弹窗之功能。

定义一个名为 keyDownHandler 的函数,它接受一个类型为 any 的参数 e,如果 visibleRef 的当前值存在,而且 e 的 code 属性值为"Escape",那么调用 props 的 onHide 方法并返回,如果 e 的这个属性值是"ArrowDown",则调用 setIndex 方法,传入一个回调函数,函数内返回 oldIndex 加 1 后与 usersRef 当前所引用数组长度减 1 的最小值并返回,如果 e 的属性值是"ArrowUp"爱游戏登录入口网页版平台,则调用 setIndex 方法,传入一个回调函数返回 oldIndex 减 1 后与 0 的最大值并返回,如果 e 的属性值是"Enter",而且 indexRef 的当前值不为 undefined 以及 usersRef 当前所引用数组存在。

indexRef.current

请你提供完整准确的句子内容,以便我能按照要求进行改写。你给出的这段内容似乎不完整。

indexRef.current

大致的原理图:

具体我们详细分步来看看。

8.1 把原来的 TextNode 进行切块

假如文本是:“请帮我泡一杯咖啡@ABC,这是后面的内容”。

据此,我们得依据光标所处位置,替换掉@ABC那部分文本,接着将其划分成前后两块,一块是:『请帮我泡一杯咖啡』,另一块是:『这是后面的内容』。

8.2 创建 At 标签

要达成删除键能够将全部删除予以实现,就得把 at标签的内容进行包裹。

这是由第一版所撰写的一个标签,然而若直接进行使用的话,会存在有一点小问题,故而将其留存下来,以便后续再次讨论。

创建一个函数名为 `createAtButton`,它接受一个类型为 `User` 的参数 `user`,在函数内部,声明一个变量 `btn`,它通过 `document` 对象来新建一个标签名为 `"span"` 的元素,接着,将 `btn` 的样式设置属性 `display` 设置为 `"inline-block"`,再把 `btn` 的数据集属性 `user` 设置为将 `user` 进行 JSON 字符串化后的结果爱游戏app官方网站登录入口,然后,给 `btn` 的类名属性 `className` 设置为 `"at-button"`,又设置 `btn` 的内容是否可编辑属性 `contentEditable` 为 `"false"`,最后,给 `btn` 的文本内容属性 `textContent` 设置为一个模板字符串,模板字符串的值是由 `"@"` 加上 `user` 的 `name` 属性值组成,最后返回 `btn` 这个元素。

8.3 把标签插进去

起初:能够得到 focusNode 节点,接着,则能够获取其的父节点以及兄弟节点。

目前所要做的是,将旧有的文本节点予以删除,接着在原本所处的位置,依照顺序逐个插入,即『请帮我泡一杯咖啡』,此外,再插入【@ABC】,最后,还要插入『这是后面的内容』。

具体来看看代码:

8.4 重置光标的位置

在我们进行这一顿操作之前,由于原来的文本节点已然丢失,故而我们的光标也随之失去了。在这个时候,那就需要重新将光标定位到 at 标签之后。

就是这样简单地讲,要把光标放置使得其处于 nextTextNode 节点之前才行,就是这样如此这般:

// 去创建一个 Range,并且去调整光标,const range = newRange();,range.setStart(nextTextNode, 0);,range.setEnd(nextTextNode, 0);,const selection = window.getSelection();,selection?.removeAllRanges();,selection?.addRange(range)。

8.5 优化 at 标签

第2步中,我们创建了 at 标签,但是会有点小问题。

在这个时候,光标定位到了那个被称作『按钮边框内』的地方,然而光标的实际位置却是正确无误的。

为了对这个问题予以优化,首先在脑海当中浮现出来的念头是,于nextTextNode里增添一个『0宽字符』,也就是——\u200b。

创建一个新的文本节点,其内容为添加了零宽字符的剩余切片部分,也就是“\u200b”加上剩余切片内容。定位光标时,要移动一位,于是创建一个新的范围对象。通过该对象,将起始位置设置为新文本节点的第1项,将结束位置也设置为新文本节点的第1项。

但是,事情没那么简单。因为我发现如果往前可能也会这样……

最后再思考一下:把那个内容区域搞得更宽一些难道不妥吗?比如说在左右两边分别加上一些空格,如此这般之后,就使得标签被包裹起来形成了一层新的样子……

首先,定义一个名为 `createAtButton` 的函数,该函数接受一个类型为 `User` 的参数 `user`,然后,在函数内部,创建一个名为 `btn` 的元素,创建完毕后,设置 `btn` 的样式显示为内联块级元素,接着,将 `user` 以 JSON 字符串形式存储在 `btn` 的数据集里,之后,给 `btn` 添加一个类名为为 `at-button`,给 `btn` 设置内容不可编辑,再给 `btn` 设置文本内容,文本内容为 `@` 加上 `user` 的名字,接着,创建一个名为 `wrapper` 的元素,创建完毕后,设置 `wrapper` 的样式显示为内联块级元素,给 `wrapper` 设置内容不可编辑,再创建一个名为 `spaceElem` 的元素,设置该元素空白间距属性为保留空白符,给 `spaceElem` 设置文本内容爱游戏最新官网登录入口,文本内容为特殊字符,给 `spaceElem` 设置内容不可编辑,再克隆 `spaceElem` 得到 `clonedSpaceElem`,将 `spaceElem` 添加到 `wrapper` 中,将 `btn` 添加到 `wrapper` 中,将 `clonedSpaceElem` 添加到 `wrapper` 中,上述操作完成后,最后返回 `wrapper`。

穷人粗糙版 at 人,最终完结~

9、小结一下

Web前端中,富文本存在的问题着实相当多,先前并未怎么对这一部分的知识进行过了解。尽管整个进程看上去显得颇为粗糙,然而技术原理便是如此。

不完善的地方很多,有更好的方式可以共同讨论下。

要是怀有那份兴趣,同样能够前往 Playground 去体验体会(点击此处即能进入)。

上面链接打开后是这样的,可以在线试试本文代码的运行效果:

10、参考资料

Selection的W3C官方API手册

现代JavaScript 教程

Range的MDN在线API手册

为 Android 端 IM 应用实现@人功能,此次实现方式是模仿微博、QQ 、微信,实现过程具备零入侵特点,同时还拥有高可扩展性。

网友留言(0)

评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。