react18-webchat网页聊天实例|React Hooks+Arco Design仿微信桌面端
<p><span style="font-size: 18px; font-family: 宋体, "Songti SC"; 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: "courier new", 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>
<<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: () => 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>={()=>setConfirmVisible(<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>
<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)"><</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)">></span><span style="color: rgba(0, 0, 0, 1)">
包裹需要滚动的内容块。。。
</span><span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">RScroll</span><span style="color: rgba(0, 0, 255, 1)">></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(<App />)</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><>
<HashRouter>
<Router />
</HashRouter>
</>
<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 = () =><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><div className="rc__container flexbox flex-alignc flex-justifyc" style={{'--themeSkin': authState.skin}}>
<div className="rc__layout flexbox flex-col"><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)"> <div className="rc__layout-header">顶部栏</div> </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">}
</span><div className="rc__layout-body flex1 flexbox"><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><Menu />
<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><Aside />
<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><div className="rc__layout-main flex1 flexbox flex-col"><span style="color: rgba(0, 0, 0, 1)">
{ lazyload(</span><Outlet />) }
</div>
</div>
</div>
</div>
<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(() => import('@views/index'<span style="color: rgba(0, 0, 0, 1)">))
const Contact </span>= lazy(() => import('@views/contact'<span style="color: rgba(0, 0, 0, 1)">))
const Uinfo </span>= lazy(() => import('@views/contact/uinfo'<span style="color: rgba(0, 0, 0, 1)">))
const NewFriend </span>= lazy(() => import('@views/contact/newfriend'<span style="color: rgba(0, 0, 0, 1)">))
const Chat </span>= lazy(() => import('@views/chat/chat'<span style="color: rgba(0, 0, 0, 1)">))
const ChatInfo </span>= lazy(() => import('@views/chat/info'<span style="color: rgba(0, 0, 0, 1)">))
const RedPacket </span>= lazy(() => import('@views/chat/redpacket'<span style="color: rgba(0, 0, 0, 1)">))
const Fzone </span>= lazy(() => import('@views/my/fzone'<span style="color: rgba(0, 0, 0, 1)">))
const Favorite </span>= lazy(() => import('@views/my/favorite'<span style="color: rgba(0, 0, 0, 1)">))
const Setting </span>= lazy(() => import('@views/my/setting'<span style="color: rgba(0, 0, 0, 1)">))
const Error </span>= lazy(() => 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 = () =><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><div className="rcLoading">
<Spin size="20" tip='loading...' />
</div>
<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 =><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 新增了<Suspense>组件,让你可以“等待”目标代码加载,并且可以直接指定一个加载的界面,让它在用户等待的时候显示</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> <Suspense fallback={<SpinLoading />}>{children}</Suspense><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 }) =><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><Navigate to="/login" replace={<span style="color: rgba(0, 0, 255, 1)">true</span>} />
<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><RouterAuth><RouterLayout /></RouterAuth><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: <Index /> },
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 通讯录模块</span>
{ path: '/contact', element: <Contact /> },
{ path: '/uinfo', element: <Uinfo /> },
{ path: '/newfriend', element: <NewFriend /> },
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 聊天模块</span>
{ path: '/chat', element: <Chat /> },
{ path: '/chatinfo', element: <ChatInfo /> },
{ path: '/redpacket', element: <RedPacket /> },
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 我的模块</span>
{ path: '/fzone', element: <Fzone /> },
{ path: '/favorite', element: <Favorite /> },
{ path: '/setting', element: <Setting /> },
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 404模块 path="*"不能省略</span>
{ path: '*', element: <Error /> }
<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: <Login /> },
{ path: '/register', element: <Register /> }
<span style="color: rgba(0, 0, 0, 1)">]
const Router </span>= () =><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>=><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) =><span style="color: rgba(0, 0, 0, 1)"> set({isLogged: data.isLogged, token: data.token}),
setCollapse: (v) </span>=><span style="color: rgba(0, 0, 0, 1)"> set({collapse: v}),
setSkin: (v) </span>=><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(() => 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)"><</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)">/></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 = () =><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 && sel.rangeCount > 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) =><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 &&<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 && 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 < 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> </p>
</div>
<div id="MySignature" role="contentinfo">
本文为博主原创文章,未经博主允许不得转载,欢迎大家一起交流 QQ(282310962) wx(xy190310)<br><br>
来源:https://www.cnblogs.com/xiaoyan2017/p/17695193.html
頁:
[1]