0
点赞
收藏
分享

微信扫一扫

我开发了个:所有数据只保存在 localStorage 的实用备忘录


背景

我时常把 Sublime Text 当做本地的备忘录,临时存一些文本、代码、思路等,非常方便。

特点在于:

  • 信息只保存本地,非常安全。
  • 响应速度非常快。
  • 编辑器好用,支持搜索、正则替换、多光标模式。
  • 自动保存,退出后,未主动保存的文件也会存下来。

但是偶尔它会弹窗提示付费,而且有一次我没保存的内容差点丢了(因为进程崩溃原因),结果是搜了教程,才花费九牛二虎之力在某个神秘文件夹里找到。

所以我就想,能否做个网页版的类似的工具呢?

我找到了 ​​Ace​​,一个Web编辑器,我可以用它实现一个 网页版 Sublime Text。

我开发了个:所有数据只保存在 localStorage 的实用备忘录_开发者

我开发了个:所有数据只保存在 localStorage 的实用备忘录_html_02

理想效果

我期望这个「网页版 Sublime Text」有跟 Sublime Text 一样的特点:

  • 信息保存本地,安全。
  • 响应速度快。
  • 编辑器好用,支持搜索、正则替换、多光标模式。
  • 自动保存,退出后,未主动保存的文件也会存下来。

我开发了个:所有数据只保存在 localStorage 的实用备忘录_前端_03

开发难点

  1. 备忘录肯定不能只有1个tab(文件),我需要多个文件。既然这样,还需要支持新建、修改、删除、重命名。
  2. 信息都存在本地,需要好好规划localStorage。
  3. 存储的信息不止是内容纯文本,还需要把光标位置存下来,更方便。

localStorage 规划

详见图:

我开发了个:所有数据只保存在 localStorage 的实用备忘录_JavaScript_04

所有的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

引用下这个不错的回答:

我开发了个:所有数据只保存在 localStorage 的实用备忘录_前端_05

意思是说,如果你想监听​​contenteditable​​​元素的​​onchange​​​事件,其实是无效的,你只能监听​​oninput​​事件。

我挺喜欢这个原生的编辑属性的。如果不用,在 React 或 Vue 中可以控制状态来渲染​​input​​​或​​div​​​。但是如果希望用原生JS写一点简单的东西,那么状态控制是需要避免的事情,会让代码变得更多更复杂,而 ​​contenteditable​​ 刚好就解决了这个复杂的问题。

目录分割线样式

参考文章​​《我又来帮掘金修专栏bug了,顺便教你个超牛逼的分割线CSS!》​​。

监听内容改动

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​​

源码:​​github.com/HullQin​​

备注:目前源码主要是为了实现功能,晚上熬夜写出来的,没有经过设计,所以会偏过程化。以后需要加功能时,我再来优化。

写在最后

举报

相关推荐

0 条评论