年前有幸和其他几个优秀的同事一起开发完成im项目开发。im项目在公司所有产品线中是功能最少但是细节多开发难度大。一方面来自于产品形态属于富交互,第二点来自优秀作品的比较:比如钉钉,微信,QQ等等。往往一个看似平常的功能,实现起来要多端配合,难度的跨度比较大。

先看效果: 线上地址 https://im.clouderwork.com

im项目虽然历经坎坷,单回头看也很精彩其中有很多可圈可点之处,不方便一一道来,随手拿出几个经典的场景浅析下使用的方案,希望有所交流和帮助

Contenteditable Vs Textarea

发消息的输入框可以有两种方案:

方案1: 使用div contenteditable

方案2: 使用textarea

分析下两种方案的利弊:方案1 可以插入任意标签,对于im 多个标签就可以干很多事,比如标记@的信息,展示表情。项目初期我们使用的此方案,美中不足不兼容其他端,也就是前端可以随意展示html 但是安卓端和ios端不能在消息体中带有html标签 方案2: 是纯 字符串解决方案,兼容其他端,变成了操作字符串。相应的表情不能展示成图片,只能[开心]这样展示。几经沟通最后确定还是采用了方案2

下面分享下方案2的难点

如果是方案1 我们可以@人的时候 加入一个span 标签 <span contenteditable="false" data-id='xxxxx'>@路飞</span> 这样我们解析的时候非常好解析,还有个问题,单我们删除@的人的时候 是整个@路飞一起删除,而不是一个个字删除。

在纯字符串中去解决这个问题就不好解决了。顺带还有一些其他问题比如:键盘左右键,经过 @路飞的时候要跳过去,不能停留在 @路飞 之间,不能破坏@的结构。 鼠标单击要让光标落在 @路飞 的前面或者后面。

该功能参照钉钉客户端的实现。

为了更好的交流,我把代码也开源出来,希望能有更好的解决方案。体验效果:http://kouyun.me/Vue-textarea/

https://github.com/Yunkou/Vue-textarea/tree/gh-pages 代码在此

说下大概的思路:

整个操作都是在计算光标和字符串index 的关系。会维护一个数组存储@的信息,记录这个@开始的位置,结束的位置和一些特征信息比如id。

核心代码:

1
2
3
4
5
6
7
8
9
const REX = /\s@\S+\s/g
let arr
let posArr = []
while ((arr = REX.exec(val)) !== null) {
let pos = {
start: arr.index,
end: REX.lastIndex
}
posArr.push(pos)

利用正则对象exec 方法 不断迭代。找到开始点和结束点。

单其他相关操作的时候,遍历光标是否在这个区间内,一旦进入这个区间立即给予相应的处理。

第二个难点textarea 是没法拿到光标的坐标
这里比较绕,不是光标的位置 index,而是定位 fixed position 也就是相对页面位置。这个该如何计算,一直是个难点,这个问题用来解决,手动输入@的时候,正确的在光标定位处出现 @的人员列表。光标是无法计算的。后来想了一个代理布局的方式,即在 textarea 下方 用一个pre标签来模拟 textarea 标签布局。当textarea监听到输入@(修饰键50且shift修饰键值为true),在pre光标处(字符串index)插入 空span标签。然后用dom树找到插入的空span标签计算出位置,并映射到textarea正确的位置。

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (!e.shiftKey) { return }
let index = document.querySelector('textarea').selectionStart
let tmpStr = this.textValue
let pre = document.querySelector('.textarea')
pre.innerHTML = tmpStr.substring(0, index) + '<span></span>' + tmpStr.substring(index, tmpStr.length)
let span = pre.querySelector('span')
let left = span.offsetLeft - pre.offsetLeft
let bottom = 150 - (span.offsetTop - pre.offsetTop)
// 小于0时根据滚动高度来计算位置
if (bottom < 0) {
let scrollHeight = pre.scrollHeight
bottom = scrollHeight - 10 - (span.offsetTop - pre.offsetTop)
}
this.atListPos = {
left: left + 10 + 'px',
bottom: bottom + 'px'
}

im可以说是技术的一个无底洞,处理的技术细节特别多,随着慢慢的打磨越来越喜欢上这个项目,大胆实践,谨慎求证。

文写于2017除夕夜,祝自己和朋友亲人新年快乐。