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

本文介绍了如何在Vue3项目中实现图片小表情在输入框中的显示与发送,通过发送含义字符串代替图片,以及如何处理输入回显和安全性问题。作者还分享了如何在父组件和子组件间传递数据和处理表情替换的过程。
摘要由CSDN通过智能技术生成

之前做项目实现聊天功能,有几个功能点我觉得挺复杂的。今天我来说一下,我是如何实现图片小表情在输入框中显示,发送给后端时只发送一个含义字符串如:[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>

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

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风险!

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

  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

旅行中的伊蕾娜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值