vue3聊天输入框

3,026 阅读3分钟

demo

最近写的IM应用,支持视频通话、多语言、主题切换等;
体验地址:fffuture.top
github地址: github.com/fffuture/IM

准备工作

下载掘金的emoji图片🙈,以便后续使用,就是下边这些: 1632406277(1).jpg

有142个emoji图片,一个个下载有点麻烦,那就写个爬虫去批量下载。在e0172e9.js文件下有所需的信息:

image.png

文件中包含emoji的中文名称和文件名称:

image.png

image.png

用node把它下载保存到本地, 关键代码:

/**
* @description 获取并保存包含emoji相关信息的js文件,比如emoji名字和地址
*/
function saveJSFile() {
    return new Promise(async (resolve, reject) => {
      try {
        fs.writeFile("./emoji/jsFile.js", "111", err => {
          if(!err) console.log("创建jsFile.js成功~");
        });
        const dest = fs.createWriteStream('./emoji/jsFile.js');
        let res = await fetch('https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/90efd53.js');
        let reader = res.body.pipe(dest);
        reader.on('finish', () => {resolve("success");});
      } catch (error) {
        reject("false");
      }
    })
}

我期望的目标数据结构:

[
    {
        "url":string, // emoji原始的图片地址
        "CN":string,  // emoji中文名称
        "EN":string,  // emoji英文名称
    },
    ...
]

缺少了英文名称,文件中又不提供,那就写个爬虫实时调用翻译接口,将中文翻译成英文:

const fetch = require('node-fetch');
const cheerio = require('cheerio');
/**
 * @description 翻译函数,多个单词使用小写并下划线隔开,例子:微笑你好 -> Smile hello -> smile_hello
 * @param {string} word 需要翻译的中文
 * @param {number} duration 延长请求时间,防止被识别为爬虫
 * @returns {Promise} 翻译完成的英文
 */
function translate(word, duration) {
    return new Promise((resolve, reject) => {
      if(!word) resolve("empty_input");

      setTimeout(() => {
        fetch(`https://m.youdao.com/translate?inputtext=${encodeURI(word)}&type=ZH_CN2EN`)
        .then(res => res.text())
        .then(body => {
          const $ = cheerio.load(body);
          let name = ($(".generate #translateResult").text() + "").trim();
          resolve((name+"").toLowerCase().replace(/ /g, '_').replace(/\,/g, ""));
        })
        .catch(err => {
          console.log("---err: ", err);
          // reject(false);
          resolve("empty_input")
        })
      }, duration)
    })
}

由于调用有道翻译mobile端接口(只有这个好使),返回的是一个html文件,所以使用cheerio库做dom解析,以便提取翻译结果。

image.png

翻译测试:

const {translate} = require('../src/utils/getEmoji');

test('翻译输入 微笑', async () => {
    let result = await translate('微笑');
    expect(result).toMatch('smile')
});
test('翻译输入 空值', async () => {
    let result = await translate('');
    expect(result).toMatch('empty_input')
})
test('翻译输入 english', async () => {
    let result = await translate(undefined);
    expect(result).toMatch('english')
})

到目前为止,对文件内容和翻译接口稍微处理就可以得到想要的数据结果:

image.png

最后一步,将emoji图片保存到本地并重属名:

async function saveImg() {
      let fetchList = [];
      for(let item of imgArr) fetchList.push(fetch(item.url));

      return new Promise((resolve, reject) => {
        try {
          let i = 0;
          Promise.allSettled(fetchList)
          .then(results => results.forEach((result) => {
            if(result.status === "fulfilled") {
              let dest = fs.createWriteStream(`./emojiEN/emoji_${imgArr[i].EN}.png`);
              result.value.body.pipe(dest);
              i++;
              console.log(`保存${imgArr[i].CN}${imgArr[i].EN}成功`);
            }else {
              console.log(`保存${imgArr[i].CN}${imgArr[i].EN}失败`, err);
            }
          }))
          .catch(err=> {
            console.log("--保存表情失败: ", err);
          });
          resolve("saved");

        } catch (error) {
          reject('---error')
        }
      });
  }

最终获取到的emoji表情:

image.png

开始写聊天输入框

先来看看最终效果:

image.png

样式方面比较简单,除了输入框关键css,其他就此不做赘述。

.input-container {
    box-sizing: border-box;
    padding: 0.53rem 0;
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    width: 100%;
    min-height: 2.3rem; /* 整个footer的最小高度 */
    max-height: 7.1rem; /* 最大高度 */
    background-color: var(--Dialogue-footer-bg);
}
.rich-text {
    box-sizing: border-box;
    padding: .5rem .5rem;
    flex-grow: 1;
    flex-shrink: 1;
    min-height: 1.3rem;  /* 输入框的最小高度 */
    max-height: 6.04rem; /* 最大高度 */
    color: var(--Dialogue-color);
    background-color: var(--Dialogue-input-bg);
    border-radius: .2rem;
    text-align: left;
    line-height: 1.5;
    word-break: break-all;
    outline: none;
    overflow: auto;
}

输入框使用的是div的contentEditable属性,由于div没有value属性,但可编辑div有input事件,所以模拟个类似的v-model的功能,关键代码如下:


<div ref="richText" class="rich-text" contentEditable=true @click="inputMsg" @input="textChange"></div>

/**
 * @description 当contentEditable中内容发生变化时,将值实时赋值给content:模拟数据双向绑定(v-model)
  */
function textChange(e:any) {
    content.value = e.srcElement.innerHTML;
}

 /**
  * @description 点击表情,将对应字符串推进输入框中 例子: 点击‘微笑表情’ -> 输入框中增加: [微笑]
  */
function pushImg(emoji:Emoji) {
    richText.value!.innerHTML += `[${emoji.CN}]`;
    content.value += `[${emoji.CN}]`;
}

富文本的处理比较麻烦,在PC端,兼容性和表现都还不错,但在移动端,就有点一言难尽了。也参考过掘金的输入框,在PC端表现良好,但在mobile端,‘表情’选项直接没了。思量了很久都没有一个完美的解决方案,最后妥协采用了:在输入框中,使用‘[emoji中文名称]’来替代表情图片,在对话框中正常显示图片。

为什么不选择input之类的作为输入框:

  • input:无法显示多行
  • textarea:不能随输入的内容自动控制高度,或者说实现起来很麻烦且不合理
  • contentEditable插入表情dom节点:在mobile端,主要是光标问题,在点击表情图片和输入框空白位置时,都会有问题。