起因
用过一款浏览器笔记插件,鼠标选中文字后,会弹出按钮,点击按钮可以在对应位置记笔记,保存笔记后,选中的文字会高亮,再次打开该网页仍可以查看高亮文字,以及对应的笔记。
但是使用过程中发现,当笔记太多,页面渲染会变得很慢,插件经常出现崩溃的情况,应该是所有笔记数据都存储在localstorage所致。
因此我准备参照该插件的功能,对存储性能进行优化,开发一个改进版的网页笔记插件。
失败的开发过程
我设想的方案是,当为选中的文字添加笔记以后,将选中文字用<span></span>包裹起来,然后设置文字的背景颜色,从而达到高亮的效果,但是这样存在两个问题:
- 会导致原本的文本排版变化
- 有些跨dom选中文字的情况无法用该方法
开源项目学习
因为被卡在"选中区域高亮"这一点上,于是在找了一些开源项目,发现context-note这一插件功能比较完备,并且代码公开,遂结合AI对代码进行学习。
选中文本高亮
首先代码注册了一个监听器,用于监听鼠标抬起事件,判断是否有文本被选中:
js
// 监听`mouseup`事件判断是否有文本被选中
document.addEventListener("mouseup", (e) => {
clearLogoIcon();
// 如果有文本被选中,解析其`rects`和`texts`
const { rects, texts } = parseRectsAndTextFromSelection();
const text = getFormattedTextFromTextList(texts);
}
其中parseRectsAndTextFromSelection()先通过递归的方式获取选中元素最内层的文本,然后获取选中文字所占区域,在该区域创建一个div,设置颜色,透明度,遮罩在文本上方,实现高亮的效果。
代码中是通过range.getClientRects()获取选中内容所占区域,自己用下面这个例子测试了一下:

途中带颜色的是选中的文字,该函数返回的区域被圆角矩形框住,可见range.getClientRects()不仅返回了选中文本所在的最内层dom,还返回了其外层dom,因此需要排除外层dom。
js
function filterDuplicateRects(rects: Rect[]) {
const filterRectMap = new Map<string, Rect>();
for (const rect of rects) {
const key = `x:${rect.x},y:${rect.y}`;
if (!filterRectMap.has(key)) {
filterRectMap.set(key, rect);
} else {
const oriRect = filterRectMap.get(key) as Rect;
if (
(oriRect.width > rect.width && rect.width > 0) ||
oriRect.width === 0
) {
filterRectMap.set(key, rect);
}
}
}
return Array.from(filterRectMap.values());
}
该函数逻辑是:优先保留有宽度且较小的矩形;特殊处理宽度为0的矩形(可能是无效矩形):
- 初始化Map:创建一个以坐标为键的Map来存储唯一的矩形
- 遍历矩形数组:
- 为每个矩形生成键:"x:{x},y:{y}"
- 如果该坐标不存在,直接添加
- 如果坐标已存在,进行比较:
- 如果原矩形宽度 > 新矩形宽度 且 新矩形宽度 > 0,用新矩形替换
- 或者如果原矩形宽度为0,也用新矩形替换
- 返回结果:将Map的值转换为数组返回
另外还有一个函数用于排除包含关系,将外部的元素排除,保留内部元素。
保存笔记
该部分有这样一段代码:
javascript
storage.notes = await addItemToArr(StorageKeys.notes, note);
// make sure the note dom is created
setTimeout(() => {
// 2. scroll to the note item
const divNote = noteDivs.value[noteDivs.value.length - 1]?.$el;
if (divNote) {
divNote.scrollIntoView({ block: "center" });
}
// 3. focus the content editor of this note
// make sure above `scrollIntoView` is finished
setTimeout(() => {
mitt.emit("focus-editor", note.id);
});
});
不太理解此处setTimeout()的作用,而且不设置暂停时间的意义是什么。结合AI的解答,有了以下理解:
setTimeout(callback, delay)中的delay默认是0,回调函数不会立即执行,而是被放入「宏任务队列」,等待当前同步代码执行完毕、调用栈清空后,才会执行。可以确保前面的 DOM 操作、渲染行为先完成,再执行回调函数。
添加笔记使用时,使用的是chrome浏览器本地存储chrome.storage.local.set(),和普通网页localStorage固定5M不同,插件在使用浏览器本地存储时可以用 chrome.storage.local.requestQuota()申请扩容,但是该程序里面并没有这个功能,笔记存储空间为固定的5M。
后续考虑升级扩容申请机制,或者使用存储空间更大的indexDB(100M)。