第一次使用@人功能到现在已经有差不多10年了,初次使用是通过微博体验的。@人的功能现在遍布各种应用,基本上涉及社交(IM、微博)、办公(钉钉、企业微信)等场景,就是一个必不可少的功能。
最近正好在调研 IM 各种功能的技术实现方案,所以也详细地了解了下@人功能在Web网页前端的技术实现,正好借此机会给大家分享一下我所掌握的技术原理和代码实现。
微博的实现比较简单,就是通过正则匹配,最后用空格表示匹配结束,所以实现上是直接使用了textarea标签。
但是这个实现必须依赖的一个事情是:用户名必须唯一。
微博的用户名就是唯一的,所以正则所匹配到的ID,一般的可以映射到唯一的一个用户上(除非ID不存在)。不过,微博中的这个功能整体输出比较宽松,你可以构造任何不存在的ID进行@操作。
通过分析业内的主流实现,@人功能的技术实现思路大致如下:
1)监听用户输入,匹配用户以@开头的文字;
2)调用搜索弹窗,展示搜索出来的用户列表;
3)监听上、下、回车键控制列表选择,监听ESC键关闭搜索弹窗;
4)选择需要@的用户,把对应的HTML文本替换到原文本上,在HTML文本上添加用户的元数据。
一般来说,如果像平常用的Lark搜索(Lark就是“飞书”),我们是不会通过唯一的『工号』去进行搜索,而是通过名字,但是名字会出现重复,所以就不太适合用textarea的方式,而是用contenteditable,把@文本替换成HTML标签特殊化标记。
代码实现第1步:获得用户的光标位置
想要获得用户输入的字符串,然后替换进去,第一步就是需要获得用户所在的光标。要获取光标信息,那就要先了解什么是『选择(Selection) 』和『范围(Range) 』。
范围(Range)
Range本质上是一对“边界点”:范围起点和范围终点。
每个点都被表示为一个带有相对于起点的相对偏移(offset)的父 DOM 节点。如果父节点是元素节点,则偏移量是子节点的编号,对于文本节点,则是文本中的位置。即时通讯开发
例如:
let range = newRange();
然后使用 range.setStart(node, offset) 和 range.setEnd(node, offset) 来设置选择边界。
假设 HTML 片段是这样的:
<pid="p">Example: <i>italic</i> and <b>bold</b></p>
解释一下:
1)range.setStart(p, 0) :将起点设置为 <p> 的第 0 个子节点(即文本节点 "Example: ");
2)range.setEnd(p, 2) : 覆盖范围至(但不包括)<p> 的第 2 个子节点(即文本节点 " and ",但由于不包括末节点,所以最后选择的节点是 <i>)。
我们需要创建一个范围:
1)从的第一个子节点的位置 2 开始(选择 "Example: " 中除前两个字母外的所有字母);
2)到 的第一个子节点的位置 3 结束(选择 “bold” 的前三个字母,就这些),代码如下。
选择(Selection)
Range 是用于管理选择范围的通用对象。
文档选择是由 Selection 对象表示的,可通过 window.getSelection() 或 document.getSelection() 来获取。
根据 Selection API 规范:一个选择可以包括零个或多个范围(不过实际上,只有 Firefox 允许使用 Ctrl+click (Mac 上用 Cmd+click) 在文档中选择多个范围)。
其他浏览器最多支持 1 个范围。
正如我们将看到的,某些 Selection 方法暗示可能有多个范围,但同样,在除 Firefox 之外的所有浏览器中,范围最多是 1。
与范围相似,选择的起点称为“锚点(anchor)”,终点称为“焦点(focus)”。
主要的选择属性有:
1)anchorNode:选择的起始节点;
2)anchorOffset:选择开始的 anchorNode 中的偏移量;
3)focusNode:选择的结束节点;
4)focusOffset:选择开始处 focusNode 的偏移量;
5)isCollapsed:如果未选择任何内容(空范围)或不存在,则为 true ;
6)rangeCount:选择中的范围数,除 Firefox 外,其他浏览器最多为 1。
看完上面,不知道了解了没?没关系,我们继续往下。
综上所述:一般我们只有一个 Range,当我们的光标在 contenteditable 的 div 上闪动的时候,其实就有了一个 Range,这个 Range 的开始和结束位置都是一样的。
另外:我们还可以直接通过 Selection.focusNode获取到对应的节点,通过 Selection.focusOffset 获取到对应的偏移量。
代码实现第2步:获取需要@的用户
在上一节我们获得了光标在对应Node节点的偏移量,以及对应的Node节点。那么就可以通过textContent方法获取整个文本。
Web前端富文本的坑确实比较多,之前没怎么了解过这部分的知识。虽然整个过程看起来很粗糙,但是技术原理就是这样。
不完善的地方很多,有更好的方式可以共同讨论下。