我开发了个:所有数据只保存在 localStorage 的实用备忘录
背景
我时常把 Sublime Text 当做本地的备忘录,临时存一些文本、代码、思路等,非常方便。
特点在于:
- 信息只保存本地,非常安全。
- 响应速度非常快。
- 编辑器好用,支持搜索、正则替换、多光标模式。
- 自动保存,退出后,未主动保存的文件也会存下来。
但是偶尔它会弹窗提示付费,而且有一次我没保存的内容差点丢了(因为进程崩溃原因),结果是搜了教程,才花费九牛二虎之力在某个神秘文件夹里找到。
所以我就想,能否做个网页版的类似的工具呢?
我找到了 Ace,一个Web编辑器,我可以用它实现一个 网页版 Sublime Text。
理想效果
我期望这个「网页版 Sublime Text」有跟 Sublime Text 一样的特点:
- 信息保存本地,安全。
- 响应速度快。
- 编辑器好用,支持搜索、正则替换、多光标模式。
- 自动保存,退出后,未主动保存的文件也会存下来。
开发难点
- 备忘录肯定不能只有1个tab(文件),我需要多个文件。既然这样,还需要支持新建、修改、删除、重命名。
- 信息都存在本地,需要好好规划localStorage。
- 存储的信息不止是内容纯文本,还需要把光标位置存下来,更方便。
localStorage 规划
详见图:
所有的key都有个前缀memo-
,这是因为我很多工具都部署在同一个域名 tool.hullqin.cn 下,所以需要前缀区分不同网页的存储。
有个memo-meta
存储多个文件的信息,包括id、文件名、创建时间。
memo-{id}
存储文件的文本。memo-{id}-c
存储文件的光标位置。
如何保证 localStorage 有固定前缀
如果每次靠开发者自觉加前缀,是有风险的,指不定以后的哪天就忘了。所以需要一种方法,自动加固定前缀。
方法一:封装函数getItem
和setItem
,这两个函数自动调用localStorage.getItem
和localStorage.setItem
,并在调用时加前缀。
方法二:全局修改localStorage.getItem
和localStorage.setItem
函数,自动加前缀。
其中方法二更好。因为方法一还是给开发者提出了更高的要求:你必须调用getItem
和setItem
而不能调用localStorage.getItem
和localStorage.setItem
。一旦开发者调用了后者,还是可能出错。
方法二的实现可以参考文章《火爆全网的 Evil.js 源码解读》,提到了修改方式:
const PREFIX = 'memo-';
const _getItem = window.localStorage.getItem;
const _setItem = window.localStorage.setItem;
const _removeItem = window.localStorage.removeItem;
window.localStorage.getItem = function (key) {
return _getItem.call(window.localStorage, PREFIX + key);
}
window.localStorage.setItem = function (key, value) {
return _setItem.call(window.localStorage, PREFIX + key, value);
}
window.localStorage.removeItem = function (key) {
return _removeItem.call(window.localStorage, PREFIX + key);
}
如何实现左侧目录
我没有用 React 和 Vue,主要是这个函数:
const renderLists = (() => {
const titlesElement = document.getElementById('titles');
return () => {
const meta = getMeta();
titlesElement.innerHTML = meta.list.map(item => {
return `<div class="title${current === item.id ? ' active' : ''}"><button class="delete" onclick="deleteMemo(${item.id})">×</button><div id="title-${item.id}" ${current === item.id ? 'contenteditable oninput="debouncedOnInput(' + item.id + ')"' : 'onclick="changeMemo(' + item.id + ')"'}>${item.title || 'untitled'}</div></div>`;
}).join('');
};
})();
我懒得每次设置innerHTML
后再addEventListener
,所以我直接用了内联的onclick
。
另外,由于需要修改标题,我用了contenteditable
属性和oninput
事件。
并且为了避免oninput
频繁触发,我使用了debouncedOnInput
做防抖。
这样,每次玩家修改标题,都会调用debouncedOnInput
来修改 localStorage 中的 meta 信息。
关于contenteditable
引用下这个不错的回答:
意思是说,如果你想监听contenteditable
元素的onchange
事件,其实是无效的,你只能监听oninput
事件。
我挺喜欢这个原生的编辑属性的。如果不用,在 React 或 Vue 中可以控制状态来渲染input
或div
。但是如果希望用原生JS写一点简单的东西,那么状态控制是需要避免的事情,会让代码变得更多更复杂,而 contenteditable
刚好就解决了这个复杂的问题。
目录分割线样式
监听内容改动
Ace 暴露了一些事件,我们可以监听。例如:change
表示内容变化。所以我需要监听这个事件,内容变化时,把最新内容存入 localStorage。
但是光标改变时,其实也需要存入 localStorage。Ace 并没有暴露光标改变相关事件,只有mouseup
事件可以参考。另外还需要监听方向键
,所以我自己给dom元素添加了keyup
事件。
由于事件监听了这么多,会频繁触发,所以需要防抖,频繁触发的,只触发一次就好。
editor.addEventListener('change', debouncedOnChange);
editor.addEventListener('mouseup', debouncedOnChange);
editor.container.addEventListener('keyup', debouncedOnChange);
使用地址 & 源码
使用地址:tool.hullqin.cn/memo.html
备注:目前源码主要是为了实现功能,晚上熬夜写出来的,没有经过设计,所以会偏过程化。以后需要加功能时,我再来优化。
写在最后
我是HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,联系我,交个朋友),转发本文前需获得作者HullQin授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋、象棋等游戏,不收费无广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这2个专栏里分享:《教你做小游戏》、《极致用户体验》。
转载自:https://juejin.cn/post/7159964175714746382