重庆火锅底料 發表於 2023-9-12 09:43:00

react18-webchat网页聊天实例|React Hooks+Arco Design仿微信桌面端

<p><span style="font-size: 18px; font-family: 宋体, &quot;Songti SC&quot;; color: rgba(19, 159, 204, 1)"><span style="background-color: rgba(255, 255, 153, 1)">React18 Hooks+Arco-Design+Zustand</span>仿微信客户端聊天<em>ReactWebchat</em>。</span></p>
<p><span style="font-size: 12px; font-family: &quot;courier new&quot;, courier"><strong>react18-webchat</strong>基于<span style="color: rgba(51, 102, 255, 1)">react18+vite4.x+arco-design+zustand</span>等技术开发的一款仿制微信网页版聊天实战项目。实现<span style="background-color: rgba(204, 255, 255, 1)">发送带有emoj消息文本、图片/视频预览、红包/朋友圈、局部模块化刷新/美化滚动条</span>等功能。</span></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912080924547-1673621298.jpg"></p>
<h3>使用技术</h3>
<ul>
<li><span style="font-size: 12px">编辑器:vscode</span></li>
<li><span style="font-size: 12px">技术栈:react18+vite4+react-router-dom+zustand+sass</span></li>
<li><span style="font-size: 12px">组件库:@arco-design/web-react (字节跳动react组件库)</span></li>
<li><span style="font-size: 12px">状态管理:zustand^4.4.1</span></li>
<li><span style="font-size: 12px">路由管理:react-router-dom^6.15.0</span></li>
<li><span style="font-size: 12px">className拼合:clsx^2.0.0</span></li>
<li><span style="font-size: 12px">对话框组件:rdialog (基于react18 hooks自定义桌面端弹窗组件)</span></li>
<li><span style="font-size: 12px">预处理样式:sass^1.66.1</span></li>
</ul>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912081559894-1834218994.gif"></p>
<h3>项目目录结构</h3>
<p><span style="font-size: 12px">使用vite4.x创建react18项目,目录线性结构如下。</span></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912082509965-1855715045.jpg"></p>
<p><span style="font-size: 12px">react-webchat 项目全部采用react18 hooks规范编码开发,使用到的对话框及虚拟滚动条均是自研组件实现功能效果。</span></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912082823404-898730067.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912082836403-1204946806.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912082903188-395329049.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912082923153-2065087553.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083000372-331870986.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083029597-1992877543.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083101300-1363147249.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083117406-269471710.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083139078-1106500208.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083157612-1571380242.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083211708-800709827.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083229898-821650741.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083251643-1570776671.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083313167-2002202535.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083329351-713240627.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083349948-149744477.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083402179-945505967.jpg"></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912083429273-1539639397.jpg"></p>
<h3>react18 hooks自定义对话框+美化滚动条</h3>
<p><span style="font-size: 12px">大家看到的弹窗及滚动条组件都是自定义组件实现功能场景。</span></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912084435235-1585433165.png"></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 引入对话框组件</span>
import RDialog, { rdialog } from '@/components/rdialog'

<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 组件式调用</span>
&lt;<span style="color: rgba(0, 0, 0, 1)">RDialog
    visible</span>=<span style="color: rgba(0, 0, 0, 1)">{confirmVisible}
    title</span>="标题信息"<span style="color: rgba(0, 0, 0, 1)">
    content</span>="对话框内容信息"<span style="color: rgba(0, 0, 0, 1)">
    closeable
    shadeClose</span>={<span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">}
    zIndex</span>="2050"<span style="color: rgba(0, 0, 0, 1)">
    dragOut
    maxmin
    btns</span>=<span style="color: rgba(0, 0, 0, 1)">{[
      {text: </span>'取消', click: () =&gt; setConfirmVisible(<span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">)},
      {text: </span>'确定'<span style="color: rgba(0, 0, 0, 1)">, click: handleInfo}
    ]}
    onClose</span>={()=&gt;setConfirmVisible(<span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">)}
</span>/&gt;

<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 函数式调用</span>
<span style="color: rgba(0, 0, 0, 1)">rdialog({
    title: </span>'标题信息'<span style="color: rgba(0, 0, 0, 1)">,
    content: </span>'对话框内容信息'<span style="color: rgba(0, 0, 0, 1)">,
    closeable: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
    shadeClose: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
    zIndex: </span>2050<span style="color: rgba(0, 0, 0, 1)">,
    dragOut: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
    maxmin: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
    btns: [
      {text: </span>'取消'<span style="color: rgba(0, 0, 0, 1)">, click: rdialog.close()},
      {text: </span>'确定'<span style="color: rgba(0, 0, 0, 1)">, click: handleInfo}
    ]
})</span></pre>
</div>
<p><span style="font-size: 12px">react-scrollbar美化系统滚动条调用非常简单,需要包裹需要滚动的内容块即可快速生成一个虚拟滚动条。</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">// 引入滚动条组件
import RScroll from '@/components/rscroll'

</span><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">RScroll </span><span style="color: rgba(255, 0, 0, 1)">autohide maxHeight</span><span style="color: rgba(0, 0, 255, 1)">={100}</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span><span style="color: rgba(0, 0, 0, 1)">
    包裹需要滚动的内容块。。。
</span><span style="color: rgba(0, 0, 255, 1)">&lt;/</span><span style="color: rgba(128, 0, 0, 1)">RScroll</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span></pre>
</div>
<h3>main.jsx入口配置</h3>
<div class="cnblogs_code">
<pre>import React from 'react'<span style="color: rgba(0, 0, 0, 1)">
import ReactDOM from </span>'react-dom/client'<span style="color: rgba(0, 0, 0, 1)">
import App from </span>'./App.jsx'<span style="color: rgba(0, 0, 0, 1)">
import </span>'@arco-design/web-react/dist/css/arco.css'<span style="color: rgba(0, 0, 0, 1)">
import </span>'./style.scss'<span style="color: rgba(0, 0, 0, 1)">

ReactDOM.createRoot(document.getElementById(</span>'root')).render(&lt;App /&gt;)</pre>
</div>
<h3>App.jsx配置</h3>
<div class="cnblogs_code">
<pre>import { HashRouter } from 'react-router-dom'

<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 引入useRoutes集中式路由配置文件</span>
import Router from './router'

<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> App() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
      </span>&lt;&gt;
            &lt;HashRouter&gt;
            &lt;Router /&gt;
            &lt;/HashRouter&gt;
      &lt;/&gt;
<span style="color: rgba(0, 0, 0, 1)">    )
}

export </span><span style="color: rgba(0, 0, 255, 1)">default</span> App</pre>
</div>
<h3>react18-router-dom配置</h3>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912085854147-2030833762.png"></p>
<p><span style="font-size: 12px">自定义路由占位模板,有点类似vue里面的router-view占位。</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 路由占位模板(类似vue中router-view)</span>
const RouterLayout = () =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    const authState </span>=<span style="color: rgba(0, 0, 0, 1)"> authStore()
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
      </span>&lt;div className="rc__container flexbox flex-alignc flex-justifyc" style={{'--themeSkin': authState.skin}}&gt;
            &lt;div className="rc__layout flexbox flex-col"&gt;<span style="color: rgba(0, 0, 0, 1)">
                {</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> &lt;div className="rc__layout-header"&gt;顶部栏&lt;/div&gt; </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">}
                </span>&lt;div className="rc__layout-body flex1 flexbox"&gt;<span style="color: rgba(0, 0, 0, 1)">
                  {</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> 菜单栏 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">}
                  </span>&lt;Menu /&gt;
<span style="color: rgba(0, 0, 0, 1)">
                  {</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> 中间栏 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">}
                  </span>&lt;Aside /&gt;
<span style="color: rgba(0, 0, 0, 1)">
                  {</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> 主内容区 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">}
                  </span>&lt;div className="rc__layout-main flex1 flexbox flex-col"&gt;<span style="color: rgba(0, 0, 0, 1)">
                        { lazyload(</span>&lt;Outlet /&gt;) }
                  &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
      &lt;/div&gt;
<span style="color: rgba(0, 0, 0, 1)">    )
}</span></pre>
</div>
<p><strong>路由配置文件</strong></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* react-router路由配置 by HS Q:282310962
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">

import { lazy, Suspense } from </span>'react'<span style="color: rgba(0, 0, 0, 1)">
import { useRoutes, Outlet, Navigate } from </span>'react-router-dom'<span style="color: rgba(0, 0, 0, 1)">
import { Spin } from </span>'@arco-design/web-react'<span style="color: rgba(0, 0, 0, 1)">

import { authStore } from </span>'@/store/auth'

<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 引入路由页面</span>
import Login from '@views/auth/login'<span style="color: rgba(0, 0, 0, 1)">
import Register from </span>'@views/auth/register'<span style="color: rgba(0, 0, 0, 1)">
const Index </span>= lazy(() =&gt; import('@views/index'<span style="color: rgba(0, 0, 0, 1)">))
const Contact </span>= lazy(() =&gt; import('@views/contact'<span style="color: rgba(0, 0, 0, 1)">))
const Uinfo </span>= lazy(() =&gt; import('@views/contact/uinfo'<span style="color: rgba(0, 0, 0, 1)">))
const NewFriend </span>= lazy(() =&gt; import('@views/contact/newfriend'<span style="color: rgba(0, 0, 0, 1)">))
const Chat </span>= lazy(() =&gt; import('@views/chat/chat'<span style="color: rgba(0, 0, 0, 1)">))
const ChatInfo </span>= lazy(() =&gt; import('@views/chat/info'<span style="color: rgba(0, 0, 0, 1)">))
const RedPacket </span>= lazy(() =&gt; import('@views/chat/redpacket'<span style="color: rgba(0, 0, 0, 1)">))
const Fzone </span>= lazy(() =&gt; import('@views/my/fzone'<span style="color: rgba(0, 0, 0, 1)">))
const Favorite </span>= lazy(() =&gt; import('@views/my/favorite'<span style="color: rgba(0, 0, 0, 1)">))
const Setting </span>= lazy(() =&gt; import('@views/my/setting'<span style="color: rgba(0, 0, 0, 1)">))
const Error </span>= lazy(() =&gt; import('@views/404'<span style="color: rgba(0, 0, 0, 1)">))

import Menu from </span>'@/layouts/menu'<span style="color: rgba(0, 0, 0, 1)">
import Aside from </span>'@/layouts/aside'

<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 加载提示</span>
const SpinLoading = () =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
      </span>&lt;div className="rcLoading"&gt;
            &lt;Spin size="20" tip='loading...' /&gt;
      &lt;/div&gt;
<span style="color: rgba(0, 0, 0, 1)">    )
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 延迟加载</span>
const lazyload = children =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> React 16.6 新增了&lt;Suspense&gt;组件,让你可以“等待”目标代码加载,并且可以直接指定一个加载的界面,让它在用户等待的时候显示</span>
    <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 路由懒加载报错:react-dom.development.js:19055 Uncaught Error: A component suspended while responding to synchronous input.</span>
    <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 懒加载的模式需要我们给他加上一层 Loading的提示加载组件</span>
    <span style="color: rgba(0, 0, 255, 1)">return</span> &lt;Suspense fallback={&lt;SpinLoading /&gt;}&gt;{children}&lt;/Suspense&gt;<span style="color: rgba(0, 0, 0, 1)">
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 路由鉴权验证</span>
const RouterAuth = ({ children }) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    const authState </span>=<span style="color: rgba(0, 0, 0, 1)"> authStore()

    </span><span style="color: rgba(0, 0, 255, 1)">return</span> authState.isLogged ?<span style="color: rgba(0, 0, 0, 1)"> (
      children
    ) : (
      </span>&lt;Navigate to="/login" replace={<span style="color: rgba(0, 0, 255, 1)">true</span>} /&gt;
<span style="color: rgba(0, 0, 0, 1)">    )
}

export const routerConfig </span>=<span style="color: rgba(0, 0, 0, 1)"> [
    {
      path: </span>'/'<span style="color: rgba(0, 0, 0, 1)">,
      element: </span>&lt;RouterAuth&gt;&lt;RouterLayout /&gt;&lt;/RouterAuth&gt;<span style="color: rgba(0, 0, 0, 1)">,
      children: [
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 首页</span>
            { index: <span style="color: rgba(0, 0, 255, 1)">true</span>, element: &lt;Index /&gt; },

            <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 通讯录模块</span>
            { path: '/contact', element: &lt;Contact /&gt; },
            { path: '/uinfo', element: &lt;Uinfo /&gt; },
            { path: '/newfriend', element: &lt;NewFriend /&gt; },

            <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 聊天模块</span>
            { path: '/chat', element: &lt;Chat /&gt; },
            { path: '/chatinfo', element: &lt;ChatInfo /&gt; },
            { path: '/redpacket', element: &lt;RedPacket /&gt; },

            <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 我的模块</span>
            { path: '/fzone', element: &lt;Fzone /&gt; },
            { path: '/favorite', element: &lt;Favorite /&gt; },
            { path: '/setting', element: &lt;Setting /&gt; },

            <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 404模块 path="*"不能省略</span>
            { path: '*', element: &lt;Error /&gt; }
<span style="color: rgba(0, 0, 0, 1)">      ]
    },
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 登录/注册</span>
    { path: '/login', element: &lt;Login /&gt; },
    { path: '/register', element: &lt;Register /&gt; }
<span style="color: rgba(0, 0, 0, 1)">]

const Router </span>= () =&gt;<span style="color: rgba(0, 0, 0, 1)"> useRoutes(routerConfig)

export </span><span style="color: rgba(0, 0, 255, 1)">default</span> Router</pre>
</div>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912090619869-2002427384.gif"></p>
<h3>react新状态管理器Zustand</h3>
<p><span style="font-size: 12px">这次开发react项目没有使用redux作为状态管理,而是使用支持react18 hooks新一代状态管理库Zustand。</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> NPM</span>
<span style="color: rgba(0, 0, 0, 1)">npm install zustand

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Yarn</span>
yarn add zustand</pre>
</div>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912091226349-691974103.png"></p>
<p><span style="font-size: 12px">zustand提供了内置的<strong>persist本地持久化</strong>存储管理。</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* react18状态管理库Zustand
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
import { create } from </span>'zustand'<span style="color: rgba(0, 0, 0, 1)">
import { persist, createJSONStorage } from </span>'zustand/middleware'<span style="color: rgba(0, 0, 0, 1)">

export const authStore </span>=<span style="color: rgba(0, 0, 0, 1)"> create(
    persist(
      (set, get) </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> ({
            isLogged: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
            token: </span><span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">,
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 折叠侧边栏</span>
            collapse: <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 个性换肤</span>
            skin: <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">,
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 登录数据</span>
            loggedData: (data) =&gt;<span style="color: rgba(0, 0, 0, 1)"> set({isLogged: data.isLogged, token: data.token}),
            setCollapse: (v) </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> set({collapse: v}),
            setSkin: (v) </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> set({skin: v})
      }),
      {
            name: </span>'authState'<span style="color: rgba(0, 0, 0, 1)">,
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> name: 'auth-store', // name of the item in the storage (must be unique)</span>
            <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> storage: createJSONStorage(() =&gt; sessionStorage), // by default, 'localStorage'</span>
<span style="color: rgba(0, 0, 0, 1)">      }
    )
)</span></pre>
</div>
<h3>react18-chat聊天模块</h3>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912092340195-1919576695.png"></p>
<p><span style="font-size: 12px">聊天区域支持拖拽发送图片、编辑框支持多行文本、光标处插入emoj表情符。</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">return (
    </span><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">div
      </span><span style="color: rgba(255, 0, 0, 1)">{...rest}
      ref</span><span style="color: rgba(0, 0, 255, 1)">={editorRef}
      </span><span style="color: rgba(255, 0, 0, 1)">className</span><span style="color: rgba(0, 0, 255, 1)">={clsx('editor', </span><span style="color: rgba(255, 0, 0, 1)">className)}
      contentEditable
      onClick</span><span style="color: rgba(0, 0, 255, 1)">={handleClick}
      </span><span style="color: rgba(255, 0, 0, 1)">onInput</span><span style="color: rgba(0, 0, 255, 1)">={handleInput}
      </span><span style="color: rgba(255, 0, 0, 1)">onFocus</span><span style="color: rgba(0, 0, 255, 1)">={handleFocus}
      </span><span style="color: rgba(255, 0, 0, 1)">onBlur</span><span style="color: rgba(0, 0, 255, 1)">={handleBlur}
      </span><span style="color: rgba(255, 0, 0, 1)">style</span><span style="color: rgba(0, 0, 255, 1)">={{'userSelect': </span><span style="color: rgba(255, 0, 0, 1)">'text', 'WebkitUserSelect': 'text'}}
    </span><span style="color: rgba(0, 0, 255, 1)">/&gt;</span><span style="color: rgba(0, 0, 0, 1)">
)</span></pre>
</div>
<p><strong><span style="font-size: 12px">获取输入光标位置</span></strong></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 获取光标最后位置</span>
const getLastCursor = () =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    let sel </span>=<span style="color: rgba(0, 0, 0, 1)"> window.getSelection()
    </span><span style="color: rgba(0, 0, 255, 1)">if</span>(sel &amp;&amp; sel.rangeCount &gt; 0<span style="color: rgba(0, 0, 0, 1)">) {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> sel.getRangeAt(0<span style="color: rgba(0, 0, 0, 1)">)
    }
}</span></pre>
</div>
<p><strong><span style="font-size: 12px">光标处插入内容</span></strong></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 光标处插入emoj表情符内容</span>
const insertHtmlAtCursor = (html) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    let sel, range
    </span><span style="color: rgba(0, 0, 255, 1)">if</span>(!<span style="color: rgba(0, 0, 0, 1)">editorRef.current.childNodes.length) {
      editorRef.current.focus()
    }</span>

    <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)">(window.getSelection) {
      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> IE9及其它浏览器</span>
      sel =<span style="color: rgba(0, 0, 0, 1)"> window.getSelection()

      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ##注意:判断最后光标位置</span>
      <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)">(lastCursor.current) {
            sel.removeAllRanges()
            sel.addRange(lastCursor.current)
      }

      </span><span style="color: rgba(0, 0, 255, 1)">if</span>(sel.getRangeAt &amp;&amp;<span style="color: rgba(0, 0, 0, 1)"> sel.rangeCount) {
            range </span>= sel.getRangeAt(0<span style="color: rgba(0, 0, 0, 1)">)
            range.deleteContents()
            let el </span>= document.createElement('div'<span style="color: rgba(0, 0, 0, 1)">)
            el.appendChild(html)
            </span><span style="color: rgba(0, 0, 255, 1)">var</span> frag =<span style="color: rgba(0, 0, 0, 1)"> document.createDocumentFragment(), node, lastNode
            </span><span style="color: rgba(0, 0, 255, 1)">while</span> ((node =<span style="color: rgba(0, 0, 0, 1)"> el.firstChild)) {
                lastNode </span>=<span style="color: rgba(0, 0, 0, 1)"> frag.appendChild(node)
            }
            range.insertNode(frag)
            </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)">(lastNode) {
                range </span>=<span style="color: rgba(0, 0, 0, 1)"> range.cloneRange()
                range.setStartAfter(lastNode)
                range.collapse(</span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">)
                sel.removeAllRanges()
                sel.addRange(range)
            }
      }
    } </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span>(document.selection &amp;&amp; document.selection.type != 'Control'<span style="color: rgba(0, 0, 0, 1)">) {
      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> IE &lt; 9</span>
<span style="color: rgba(0, 0, 0, 1)">      document.selection.createRange().pasteHTML(html)
    }

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 执行输入操作</span>
<span style="color: rgba(0, 0, 0, 1)">    handleInput()
}</span></pre>
</div>
<p>okay,基于react18 hooks+arco开发网页聊天项目就分享到这里,希望对大家有些帮助哈~</p>
<p><strong>最后附上两个最新uniapp/tauri跨端实例项目</strong></p>
<p><span style="font-size: 12px">https://www.cnblogs.com/xiaoyan2017/p/17507581.html</span></p>
<p><span style="font-size: 12px">https://www.cnblogs.com/xiaoyan2017/p/17552562.html</span></p>
<p><img src="https://img2023.cnblogs.com/blog/1289798/202309/1289798-20230912094117858-1811042743.gif"></p>
<p>&nbsp;</p>

</div>
<div id="MySignature" role="contentinfo">
    本文为博主原创文章,未经博主允许不得转载,欢迎大家一起交流 QQ(282310962) wx(xy190310)<br><br>
来源:https://www.cnblogs.com/xiaoyan2017/p/17695193.html
頁: [1]
查看完整版本: react18-webchat网页聊天实例|React Hooks+Arco Design仿微信桌面端