自制体积不到 2kB 的代码编辑器,areaEditor.js,增强 textarea 标签的代码编辑体验

笔记哥 / 05-17 / 49点赞 / 0评论 / 713阅读
## 介绍 一个只有2KB的代码编辑器,非常好用,相比 [Code Mirror](https://codemirror.net/ "Code Mirror") 或 [Ace](https://ace.c9.io/ "Ace") 这些成熟又著名的编辑器,可是..... 它们体积有点大,虽然大力压缩处理后也不过 100kb 左右,轻了许多。 ![image](https://cdn.res.knowhub.vip/c/2505/18/52070a86.png?G1cAAGSd87ygi5bqd%2bJRTRBIoBmwSCOolLBe7zlr3wDfH4xc8jNan7E%2f%2fKb1GUBmlyIBIysqUqBKiipeiydyVDIWkbxGAA%3d%3d) 仓库地址:[areaEditor](https://github.com/kohunglee/areaEditor) ## 快速使用 只需这样即可: ```html ``` 这样,您的网页上所有的 ` ``` 首先,我们阻止了 tab 的默认事件(跳到下一个表单元素)`e.preventDefault(); `,然后我们调用 `textarea` 元素的 `.value` api 重新为其填充内容。 然后是 `e.target.selectionStart = e.target.selectionEnd = ...` 让我们的光标位置放到应该到达的地方。 `.selectionStart` 、 `.selectionEnd` 、 `.value` 这三个 API 现在实现这样一个小功能,然后还是这三个 API ,渐渐贯彻了整个代码。一切从这里开始。 ## 缩进类型 目前,缩进格式并不统一,主流的缩进是 4 个空格,所以,就得需要能切换。 (可以试试在 https://www.ccgxk.com/cellhtmleditor.html 这个实时编辑页面进行调试下面的这些代码 ~) ```javascript ``` 代码里 `Array(indentCount + 1).join(' ')` 是一种技巧,可以返回 n 个空格,比如 `Array(5 + 1).join(' ')` 就是 5 个空格,`Array(3 + 1).join(' ')` 就等于 3 个空格。 根据这个原理,代码的收缩也实现了。不过,还是有很多坑,习惯上的坑: - 收缩后,光标的选择怎么计算? - 如果光标的 start 位于缩进,比如空格上,又该怎么计算?? - ... 增加缩进很简单,减少缩进没想到这么复杂,计算逻辑还挺抽象,花了好几个小时润色,才算完美解决,下面是最终的代码: ```javascript // TAB 键盘的处理 if (e.key === 'Tab') {e.preventDefault();if (start === end) { // 光标未选中多个字符 e.target.value = value.substring(0, start) + this.tabChar + value.substring(end); e.target.selectionStart = e.target.selectionEnd = start + this.tabLength; return;} else { var contentArr = value.split('\n'); var contentArrOriginal = value.split('\n'); var startLine = (value.substring(0, start).match(/\n/g) || []).length; var endLine = (value.substring(0, end).match(/\n/g) || []).length; if (event.shiftKey) { // 按下 Shift 键(减少缩进)。 for (var _i = startLine; _i <= endLine; _i++) { contentArr[_i] = this._removeLeadingSpaces(contentArr[_i], this.tabLength); } e.target.value = contentArr.join('\n'); var lengthDiff = contentArrOriginal[startLine].length - contentArrOriginal[startLine].trimStart().length; // 计算光标起始点位于那一行 var moveLength = Math.min(this.tabLength, lengthDiff); // 计算最小可缩进值,以防止起始位置(如行5)缩进至行4。 var limitLineNum = this._arrSum(contentArr, startLine); // 处理选区起始在缩进(空白)处的情况。 var startPoint = limitLineNum > start - moveLength - startLine ? limitLineNum + startLine : start - moveLength; e.target.selectionStart = lengthDiff > 0 ? startPoint : start; e.target.selectionEnd = end - (contentArrOriginal.join('\n').length - e.target.value.length); } else { // 单独按 Tab 键(增加缩进),这个简单,文章上面已经写了 for (var _i = startLine; _i <= endLine; _i++) { contentArr[_i] = this.tabChar + contentArr[_i]; } e.target.value = contentArr.join('\n'); e.target.selectionStart = start + this.tabLength; e.target.selectionEnd = end + this.tabLength * (startLine === endLine ? 1 : endLine - startLine + 1); }} } ``` ## 自动补全括号 但,只有这种缩进,肯定还不够,我们还需要那种写下括号后,再回车,产生的那种自动换行和缩进的效果。 ![image](https://cdn.res.knowhub.vip/c/2505/18/cd187c38.png?G1cAAMTsdJxI8hWi26hD2jvFHc2ARRpBpYT1es9Z%2byb6fgcjxWe0Pn1%2f%2bE3r00lyrspCYCgrQpAiCRdUzALYSlVT1riGAw%3d%3d) 按下回车: ![image](https://cdn.res.knowhub.vip/c/2505/18/6f7dad98.png?G1cAAMTsdJxI8hK026hD2jvFHc2ARRpBpYT1es9Z%2byb6fgdD4zNan74%2f%2fKb16SQ5X8ZCYBgbQpAiipQUhQMYqpa51LiGAw%3d%3d) 顺便把自动括号也实现。 于是哐哐一顿实现,计算还简单,但逐渐发现一个问题。 这是程序 1.0 时候的一个案例: ```html ``` 当我们用中文输入法输入时,一回车(按照习惯,应该只会输入 jtxql 这六个字符),会产生这样的效果: ![image](https://cdn.res.knowhub.vip/c/2505/18/b1da555f.png?G1cAAOQ8bZy4F8c26nBtokhoBizSCColrNd77z4N4PudkTU%2bs4%2fl58Nv%2blgOlHNNSMDICRNCoEIqxGpmgcRYLBetcU8H) ![image](https://cdn.res.knowhub.vip/c/2505/18/d6f235f8.png?G1cAAMTsdJxIPiS026hD2jvFHc2ARRpBpYT1es9Z%2byb6fheWHJ%2fR%2bvT94TetTyeYXcogYVFWhICCnKAomgKSWDEzrXENBw%3d%3d) 显然使用 `addEventListener('keydown', ... )` 是不行的。 于是又加入了 `addEventListener('input', ... )` 。`keydown` 和 `input` 是不一样的。前者是在键盘按下的一瞬间触发,在字符输入前(也因此可以阻止字符输入),而后者 `input` 是在字符输入后探测你按下了哪个按键。 ## 犯难 这就犯了难,我到底是使用哪个来监测 `enter` 键的按下。如果是前者,那我避免不了这个 bug,如果是后者,那画面会跳动一下,很诡异。 最后我还是使用后者,只不过我不是监测的按钮,而是字符 `if(lastChar === '\n'){ ... } `,然后重新生成缩进内容,里面多来一个换行符即可。 ## 自动补全 自动补全倒是没有什么好说的。唯一要说的,【若用户仍选择手动完成,则忽略】这个功能,让这个自动补全变得非常流畅。但我没想到,要判断用户有没有手动补全,竟然要判断三个布尔:前一个字符,是否属于要补全的符号,后一个字符是否等于应补全的符号,用户输入的是否等于已经补全的符号。 ```javascript var autoPairs = { '{': '}','[': ']','(': ')','"': '"',"'": "'",'`': '`', }; if (['{', '(', '[', '"', "'", '`', ']', '}', ')'].includes(lastChar) && start === end) {if(this.isPreventAuto){ this.isPreventAuto = false; return;}var pairChar = autoPairs[lastChar] || '';for (var leftBrace in autoPairs) { // 若用户仍选择手动完成,则忽略 if (leftBrace === secondLastChar && autoPairs[leftBrace] === lastChar && nextChar === lastChar) { e.target.value = value.substring(0, start) + value.substring(start + 1); e.target.selectionStart = e.target.selectionEnd = start; return; }}e.target.value = value.substring(0, start) + pairChar + value.substring(start);e.target.selectionStart = e.target.selectionEnd = start; } ``` ## 阻止补全 其实,上面的这些行为,并不能一直都有效。所以我又设立了阻止补全。不能说我们按快捷键复制粘贴的时候,也顺手补全了,也不能说我们在按删除键的时候也给补全了(o( ̄▽ ̄)d 这样就陷入死循环了 ~ 一个永远也删不掉的右括号) ```js AreaEditor.prototype.isPreventAuto = false; // 是否阻止某些自动脚本 ``` 检测到一些按键 或 粘贴事件 `addEventListener('paste', ...);` 后,就不再执行。 ## 编辑框抖动 一个不知起源于何时的 `textarea` 特性,当行数比较大时,回车会让输入框抖一下,十分影响..... 起码影响心情: (可以试试在 https://www.ccgxk.com/cellhtmleditor.html 这个实时编辑页面进行调试下面的这些代码 ~) ```html ``` ![image](https://cdn.res.knowhub.vip/c/2505/18/a25ed711.png?G1YAAMT0bJxor1HFNvqh%2f4lHQjMgkUVQKWG93nv3aUTf78KS4jP7WH4%2b%2fKWP5YScqzJIWJQVwcOggFrREmoyKWDTuKcD) 于是 ![image](https://cdn.res.knowhub.vip/c/2505/18/be4a1b84.png?G1YAAETn9LwUqALZvtMdbIlTE20GJLIIKiWs13vO2jfR9wcYNT%2bj9Rn7w19an0FidikLgaGsSF5cVFANjmQFMPbqeY0A) 这是浏览器的原生特性(bug),意义不明。 当然,这个问题好解决,只需要记录下高度,在完成我们的操作后将高度还原即可。 ## 在空行按下删除键,清空 在一个只存在缩进、空格的行,我们按下删除键,不出意外,目的只有一个,就是将这行删干净。所以,我又加上这样一个功能,在空行按下删除键,清空。 本以为只是一两行代码才能完成,最后搞了一坨: ```javascript if (e.key === 'Backspace') {var contentArr = value.split('\n');var startLine = (value.substring(0, start).match(/\n/g) || []).length;// 当前行仅包含空格和制表符if(start === end && (/^[\s\t]*$/.test(contentArr[startLine]) && contentArr[startLine] !== '')){ e.target.selectionStart = this._arrSum(contentArr, startLine) + startLine; e.target.selectionEnd = start;} } ``` 体验感大大的好。 ## 封装代码 我想把它做成一个第三方的引用库,那么我就要尽可能写的标准一点。 模仿 jQuery、Zepto 将它封装了一下。 首先是 UMD 模块化, ```javascript (function (global, factory) { if (typeof define === 'function' && define.amd) { // UMD 模式 define([], factory); // AMD } else if (typeof module === 'object' && module.exports) { module.exports = factory(); // CommonJS } else { global.AreaEditor = factory(); // 这样写,可以不用 new 关键字来调用 } }(this, function () { 'use strict';// 构造函数function AreaEditor(element, options = {indentType : { type: 'space', count: 4 }}) {..........return AreaEditor; })); ``` 第一个,是依照过去我们常用的 [requireJS](https://requirejs.org/ "requireJS") 要求的格式来定义的,这是一个模块化工具,让我们的 JS 文件们可以按需加载。虽然在 ES6 时代日薄西山,但还是能用得到。学习这个可以看阮一峰大佬的[这篇文章](https://www.ruanyifeng.com/blog/2012/11/require_js.html "这篇文章")。 第二个是 CommonJS 环境使用的,也就是服务器端。主要用于 node.js 。可以供 `require('./logger.js')` 这种语法使用。 第三个就是我们现在使用的这种方式,即浏览器直接调用。 里面的 `factory()` 是工厂函数, ` 'use strict';` 及以下就是这个函数的内容。我们只需将我们的 `AreaEditor` 函数写入即可。 `AreaEditor()` 是构造函数,通常使用大写字母开头。就好像一个对象一样,不过调用的时候需要写 new ,有了上面的 `factory()` 的处理后,不写 new 关键字也可以。 * * * ## 怎么压缩 JavaScript 代码 初步压缩,主流的有三个选择: - [Google Closure Compiler](https://developers.google.com/closure/compiler?hl=zh-cn "Google Closure Compiler") (感觉谷歌快放弃它了) - [UglifyJS](https://github.com/mishoo/UglifyJS "UglifyJS ") - [Terser](https://terser.org/ "Terser") 第一个是谷歌公司使用 Java 搞的智能压缩,可以分析代码把冗余给铲除,确保结果不变。不过效果和下面两个差不多。 第三个 Terser 我们可能都间接用过,它是 webpack 这个打包工具的默认压缩工具。其实它是在 UglifyJS 基础上迭代的。 在这里,我使用了 UglifyJS 。它也会智能将代码里的多余的地方优化,以实现尽可能小的体积: ```csharp var a=1;var a=2; // 合并重复变量 var a=2 alert('a' + 'b'); // 优化 alert("ab"); function a(){ var info = 'a' + 'b'; alert(info); } // 优化 function a(){alert("ab")} ``` 它本身是一个 node.js 库,无法直接在浏览器上运行,不过有大佬将其转化为了浏览器端。我又将其配置表和界面给翻译成中文,就是下面这个地址: https://git.ccgxk.com/jscompression/jsminifier.html ![image](https://cdn.res.knowhub.vip/c/2505/18/4dd7de73.png?G1YAAORtel6lilr2ne1wg1ZBNQMSWQSVEtbrvXefBvD9wciSn9nHivPhL32sADJzRQJGVlQkT4UMHasVTs4uUl017xk%3d) ![image](https://cdn.res.knowhub.vip/c/2505/18/b794b5e1.png?G1cAAMTsdJzIJyKl26hD2jvFHc2ARRpBpYT1es9Z%2byb6fheIxme0Pn1%2f%2bE3r04nNagaTQDIyQuCLTThptRKEGUhFUeIaDg%3d%3d) 使用它生成后的代码,还没有到 2kb 这个阶段,然后我又找到了个利用字母出现频次,构成字典,然后进行压缩的 js 压缩工具。 常出现在一些 代码高尔夫 炫技比赛里,比比谁能用更小的体积实现更复杂的功能或游戏这种比赛里,类似于 https://js13kgames.com/ 。 我把那个界面搞的漂亮了一点,然后将全局变量改了一下名,就是这个页面: https://git.ccgxk.com/jscompression/jscrush.html ![image](https://cdn.res.knowhub.vip/c/2505/18/06fbcfbc.png?G1cAAMT0bJxor1rCNvqh%2f4lHQjNgkUZQKWG93nv3aUTf78Ki8Zl9LD8fftPHckLO1RgkLMaGEFCQxTiVigAtkKQVFvd0) 这样就差不多 2kb 了。 但是,这个 js crush 压缩,其实在有 Gzip 的服务器情况下,并不是必需品,Gzip 的压缩率和这个差不多。 ## 更多的功能? 以后不会添加更多的功能了,因为没必要 ~ textarea 的特性就决定了它无法完美实现代码高亮,而自动语法又是很复杂的事情,需要大量代码,所以没有必要。 我的这个 areaEditor.js 的存在意义是,为那些极端情况下准备的:比如 在线改一些简单的代码、一些轻量级的库、一些对网络有限制的场景、一些简单的页面、一些黑白页面.... 另外,功能再多一点,就到「结界」了:毕竟体积大了,我为什么不选择更专业的?