微博文字排列工具开发与改版

2020-04-17 15:53


最近在用Vue做一些小东西。做了一个网页游戏,没做完所以还没放出来。还做了一个小工具,让文字排列在直线和斜线上,用来发酷炫的微博。在微博上宣传了一下,似乎反响很好。还生平第一次收到了别人提交的issue(解决了)。

这个微博文字排列工具功能不复杂。用Vue因为想学Vue。

要实现的功能包括:

  • 按下鼠标拖动的过程中,不断更新文字排列的预览。文字只能排列在直线和斜线的八个方向上,需要根据鼠标位置找到最近的排列方向。
  • 松开鼠标则固定当前预览。
  • 不可覆盖已经固定的文字。
  • 半角英文数字宽度是汉字的一半(其实和字体有关,大多数时候不是正好一半),需要调整。水平排列时每个单位需要放两个半角字符(如果有),竖直和斜线排列时只能放一个,需要加一个空格调整宽度(空格也往往不是汉字的一半,没法做到完美)。当然全换成全角也是一个办法,但是就会很丑,而且水平排列时浪费空间。
  • 支持撤销上次操作。
  • (改版后支持)可插入微博表情符号和Emoji字符。Emoji字符比较简单;微博表情符号本质是一个微博自定义的占位符,输入的时候是文字,发出来显示为图案。所以需要做的是在排列和预览时能识别这些占位符。目前只支持少量表情,不过再添加不难。

踩过的坑有:

开始时对预览和固定文字用了不同的数据源。每个格子里有两个<span>,分别绑定到两个数组。一个Overlay数组在鼠标拖动时更新预览文字,随时刷新。一个Canvas数组只在鼠标抬起时更新固定化的文字,不再刷新,但保留操作记录以支持逐步撤销。后来改版时合并为一个数组了。预览文字就是固定化的文字,鼠标拖动需要更新预览时,撤销上一步,重做预览。固定化就是将当前预览的路径标记为不可更新。感觉这样少了一重数据绑定,无论空间占用还是计算量都会减少。

微博表情符号就需要另一个Emoji数组。这里的Emoji不是指Emoji字符,微博表情符号是形如“[微笑]”这样的占位符,对应的是一个png图片。在识别到一个占位符之后,将其字符串本身当做一个完整的单位写入Canvas数组,让它占据一格的位置。同时将对应的图片文件路径放入Emoji数组。在html这边,如果格子对应的Emoji数组值不为空,则隐藏文字,显示图片。

而Emoji字符是另一个概念,是指类似🌷、🎁、💩、😜、👍这样的字符。它们是字符不是图片,直接当作文字处理就好。但是在JavaScript下,这些字符的长度是2,也就是说“🌷”.length === 2这样。因而每个字符都会被拆分到两个格子里,显示为乱码。就像这样:

{ width=”100%” }

解决方法是:不使用charAt()读取下一个字符,而是用codePointAt(),这样获取的字符code会正确识别这些Emoji字符。然后再用String.fromCodePoint()转回字符串。完整的代码是这样:

getChar: function(i) {
    if (this.textTrimmed.charAt(i) === "[") {
        var j = this.textTrimmed.indexOf("]", i);
        if (j > i) {
            // 这里的emoji是指微博表情,不是Emoji字符。
            var emojiText = this.textTrimmed.slice(i, j + 1);
            var emoji = this.getEmoji(emojiText); // 如果未找到,这里返回undefined。
            if (emoji) {
                return emoji.text;
            }
        }
    }
    // 下面这行不兼容Emoji字符。
    // return this.textTrimmed.charAt(i);
    // 下面这行是兼容Emoji字符的读取方法。
    return String.fromCodePoint(this.textTrimmed.codePointAt(i));
}

插入表情符号时需要读取光标位置。本来JQuery可以做,但想看一下Vue是怎么做的。首先Vue里获取DOM是通过$refs。在textarea的tag上定义ref="textarea",然后在Vue里用var textArea = this.$refs.textarea;就可以获取这个DOM。然后用selectionStartselectionEnd读取光标位置(这样好像有浏览器兼容性问题,但实际上由于另外的原因,网页在IE和Edge上已经崩了,所以兼容性问题就留给以后一次性解决)。

在光标位置插入表情之后又出现新的问题:由于修改了绑定的数据,textarea的光标自动跑到文字起始位置去了,这样就无法在同一个位置连续插入表情。所以还要重设selectionStartselectionEnd的值,将光标调整回来。但是刚开始时发现重设的值不起作用,光标还是在起始位置。研究了一下发现两个原因:

  1. 需要通过textArea.focus()获取焦点让位置调整生效,原因不明。
  2. Vue对数据的更新是异步进行的。绑定数据的修改可能会发生在光标位置调整之后。也就是说如果代码是这样的话是不行的:
var textArea = this.$refs.textarea;
var cursorStart = textArea.selectionStart;
var cursorEnd = textArea.selectionEnd;
this.textInput = this.textInput.substring(0, cursorStart) + emojiText + this.textInput.substring(cursorEnd, this.textInput.length);
textArea.focus();
textArea.selectionStart = cursorStart + emojiText.length;
textArea.selectionEnd = cursorStart + emojiText.length;

后三行调整光标位置的指令需要放在一个Callback里,等数据更新完成之后再执行。网上有人是设了一个setTimeout(),10毫秒之后调整光标,简单粗暴,实测也行得通。但其实Vue提供了一个叫$nextTick()的方法,用法是这样:

var textArea = this.$refs.textarea;
var cursorStart = textArea.selectionStart;
var cursorEnd = textArea.selectionEnd;
this.textInput = this.textInput.substring(0, cursorStart) + emojiText + this.textInput.substring(cursorEnd, this.textInput.length);
this.$nextTick(() => {
    textArea.focus();
    textArea.selectionStart = cursorStart + emojiText.length;
    textArea.selectionEnd = cursorStart + emojiText.length;
});

而Vue获取鼠标事件的方法是在tag上绑定v-on:mousedown.left="..."这样。

之后的更新会尝试在IE和Edge上修复。此外还考虑支持手机端操作。手机端无法监听鼠标按下、拖动、松开等事件了,而是要处理触摸屏相关的事件,而且好像Android和iOS还不一样。UI方面要大改。愁,没时间又懒,慢慢来吧。