前端怎么实现聊天输入框?怎么实现类似b站评论的输入并发送自定义表情包?输入回显、发送时表情包转义为[emoji]字符串、页面展示回显

前端怎么实现聊天输入框?怎么实现类似b站评论的输入并发送自定义表情包?输入回显、发送时表情包转义为[emoji]字符串、页面展示回显

之前做项目实现聊天功能,有几个功能点我觉得挺复杂的。今天我来说一下,我是如何实现图片小表情在输入框中显示,发送给后端时只发送一个含义字符串如:[emoji],然后正常回显在页面上
此demo使用vue3
实现效果图:
输入自定义表情发送并回显

声明:这只是个demo,不涉及与后端交互,不过会在该交互的地方标记,如需实际应用于项目,请根据实际情况进行改造完善!

父组件定义及逻辑实现

父组件dom定义如下,其中,输入框需要使用开启contenteditable
的div,不能使用input或者textarea。chatMsgEl为子组件,用来回显我们发送的消息结果。

  <div id="app">
    <div class="msg-box">
      <!-- 消息输入框 -->
      <div class="msg-input" ref="msgInput" contenteditable @blur="getAfterBlurIndex" @keydown.enter.prevent="sendMsg"></div>
      <!-- 表情列表 -->
      <div class="emoji-list">
        <p>表情列表</p>
        <img v-for="(emoji, key) in emojiList" :key="key" :src="emoji" @click="selectEmoji(key)" />
      </div>
      <button @click="sendMsg">发送</button>
    </div>
    <p>数据本体:{{ chatMsgRecord }}</p>
    <!-- 消息回显框 -->
    <chatMsgEl :msg="chatMsgRecord" :emojiList="emojiList"></chatMsgEl>
  </div>

定义表情包列表,我这里只有一个本地图片,根据这个格式本地添加或者后端返回都行

javascript">const emojiList = {
  "[vueLogo]": require("./assets/logo.png"),
};

然后定义几个变量,用来后续操作

let msgInput = ref(); // 输入框ref绑定

let chatMsgRecord = ref(""); // 发送的消息体

let focusNode = reactive({}); // 存储光标聚焦节点
let focusOffset = ref(0); // 存储光标聚焦偏移量
let chatInputOffset = reactive({}); // 存储光标聚焦的元素

想通过点击图片插入到输入框光标对应位置,最重要的就是在输入框失焦时获取到失焦前的光标位置,函数如下,具体不展开讲,不明白的话可以去搜一下关于getSelection的知识。

// 聊天输入框失焦时获取失焦前的光标位置
function getAfterBlurIndex() {
  if (window.getSelection) {
    let sel = window.getSelection();
    if (sel.getRangeAt && sel.rangeCount) {
      focusNode = sel.focusNode;
      focusOffset.value = sel.focusOffset;
      chatInputOffset = sel.getRangeAt(0);
      console.log("focusNode:", focusNode);
      console.log("focusOffset:", focusOffset.value);
      console.log("chatInputOffset:", chatInputOffset);
    }
  }
}

然后我们点击表情添加到输入框中,这个步骤分为:先获取输入框焦点(如果输入框从来都没有获取过焦点的话),然后创建图片标签追加到输入框div的子节点中(图片标签中的alt是后面发送时需要取出的表情字符串定义),最后输入框聚焦并将光标后移

// 获取输入框选取
function getInputSelection() {
  let sel = window.getSelection();
  // 插入内容之前输入框是否已经聚焦获得选区
  if (
    !chatInputOffset.endContainer ||
    (chatInputOffset.endContainer.className != "msg-input" &&
      chatInputOffset.endContainer.parentNode.className != "msg-input" &&
      chatInputOffset.endContainer.parentNode.parentNode.className != "msg-input")
  ) {
    chatInputOffset = document.createRange();
    //用于设置 Range,使其包含一个 Node的内容。
    chatInputOffset.selectNodeContents(document.querySelector(".msg-input"));
    //将包含着的这段内容的光标设置到最后去,true 折叠到 Range 的 start 节点,false 折叠到 end 节点。如果省略,则默认为 false .
    chatInputOffset.collapse(false);
    sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(chatInputOffset);
  }
}
// 点击选择表情
function selectEmoji(emoji) {
  getInputSelection(); // 先获得一下输入框焦点
  // 创建图片元素,回显到输入框内
  const img = `<img src="${emojiList[emoji]}" alt='${emoji}' class="emoji" style="width: 36px;height: 36px;object-fit: contain" >`;
  chatInputOffset.collapse(false); //光标移至最后
  // 创建节点并插入
  const node = chatInputOffset.createContextualFragment(img);
  let c = node.lastChild;
  chatInputOffset.insertNode(node);
  if (c) {
    chatInputOffset.setEndAfter(c);
    chatInputOffset.setStartAfter(c);
  }
  let j = window.getSelection();
  j.removeAllRanges();
  j.addRange(chatInputOffset);
}

这个时候就可以发送了,发送的步骤为:先清空发送的上一条消息,然后循环输入框子节点,查找追加进去的图片标签,取出标签上的alt赋值给chatMsgRecord变量,如果是普通文本消息就直接追加赋值。最后清空输入框中的内容即可

function sendMsg() {
  console.log("msgInput:", msgInput);
  chatMsgRecord.value = '' // 先清空一下旧消息
  msgInput.value.childNodes.forEach((element) => {
    console.log(element);
    // 如果是emoji表情图片的话,则转义
    if (element.nodeName === "IMG" && element.className === "emoji") {
      chatMsgRecord.value += element.alt;
    } else {
      chatMsgRecord.value += element.wholeText;
    }
  });
  // 清空输入框中的内容
  msgInput.value.innerHTML = "";
  msgInput.value.innerText = "";
  //在这里使用websocket把数据chatMsgRecord发给后端
  // socket.send({msg:chatMsgRecord})
}

子组件定义及逻辑实现

子组件这边的话就比较简单了,先定义好dom结构,下面讲为什么循环的消息变量需要使用split(/(<[^>]+>)/g)分割

 <template v-for="text in chatMsg.split(/(<[^>]+>)/g)">
      <template v-if="!text.startsWith('<emoji')">{{ text }}</template>
      <img v-else :src="imgSrc(text)[1]" class="emoji" />
    </template>```

拿到消息体,父组件把表情包定义传递给子组件,然后循环表情包列表,通过replace方法匹配到相对应的表情赋值给消息结果。假设此时的消息体为:文本[emoji]文本,则消息结果为:文本<emoji src=“http://xxx”>文本。

const props = defineProps(["msg", "emojiList"]);
// 注意:msg为后端返回给你的消息内容
const { msg, emojiList } = toRefs(props);

let chatMsg = ref(msg.value)
watch(
  msg,
  () => {
  // 核心语句
    for (const key in emojiList.value) {
      const reg = new RegExp("(\\" + key + ")", "g");
      chatMsg.value = msg.value.replace(reg, `<emoji src="${emojiList.value[key]}">`);
    }
  },
  { immediate: true }
);

这个时候来讲一下为什么上面循环的消息变量要使用split(/(<[^>]+>)/g)了,因为此时的消息为“文本<emoji src=“http://xxx”>文本”,通过分割就会变为[‘文本’,‘<emoji src=“http://xxx”>’,'文本],这样才能正确循环渲染。
在dom定义中,img的src使用的imgSrc函数,就是取出这个<emoji>标签里的src路径用的

function imgSrc(txt) {
  return txt.match(/<emoji.*?src="(.*?)".*?>/);
}

这样就实现了输入回显、发送转义、结果回显功能了。
之前也试过直接把匹配到的表情包变成img标签,通过v-html直接回显的,但是这样做会有XSS风险!

后面我会更新如何实现@功能、复制图片到输入框中并发送,以及撤回等功能。

转载请说明出处内容投诉
CSS教程_站长资源网 » 前端怎么实现聊天输入框?怎么实现类似b站评论的输入并发送自定义表情包?输入回显、发送时表情包转义为[emoji]字符串、页面展示回显

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买