react-native聊天室|RN版聊天App仿微信实例|RN仿微信界面
<h3 id="一项目简介">一、前言</h3><p><span style="font-size: 12px; font-family: "comic sans ms", sans-serif">9月,又到开学的季节。为每个一直默默努力的自己点赞!最近都沉浸在react native原生app开发中,之前也有使用vue/react/angular等技术开发过聊天室项目,另外还使用RN技术做了个自定义模态弹窗rnPop组件。</span></p>
<h3 id="一项目简介">一、项目简述</h3>
<p><span style="font-size: 12px; font-family: "comic sans ms", sans-serif; background-color: rgba(197, 226, 255, 1)">基于react+react-native+react-navigation+react-redux+react-native-swiper+rnPop等技术开发的仿微信原生App界面聊天室——RN_ChatRoom,实现了原生app启动页、AsyncStorage本地存储登录拦截、集成rnPop模态框功能(仿微信popupWindow弹窗菜单)、消息触摸列表、发送消息、表情(动图),图片预览,拍摄图片、发红包、仿微信朋友圈等功能。</span></p>
<h3 id="三技术栈">二、技术点</h3>
<ul>
<li><span style="font-size: 12px; font-family: "comic sans ms", sans-serif">MVVM框架:react / react-native / react-native-cli</span></li>
<li><span style="font-size: 12px; font-family: "comic sans ms", sans-serif">状态管理:react-redux / redux</span></li>
<li><span style="font-size: 12px; font-family: "comic sans ms", sans-serif">页面导航:react-navigation</span></li>
<li><span style="font-size: 12px; font-family: "comic sans ms", sans-serif">rn弹窗组件:rnPop</span></li>
<li><span style="font-size: 12px; font-family: "comic sans ms", sans-serif">打包工具:webpack 2.0</span></li>
<li><span style="font-size: 12px; font-family: "comic sans ms", sans-serif">轮播组件:react-native-swiper</span></li>
<li><span style="font-size: 12px; font-family: "comic sans ms", sans-serif">图片/相册:react-native-image-picker</span></li>
</ul>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">{
</span>"name": "RN_ChatRoom"<span style="color: rgba(0, 0, 0, 1)">,
</span>"version": "0.0.1"<span style="color: rgba(0, 0, 0, 1)">,
</span>"aboutMe": "QQ:282310962 、 wx:xy190310"<span style="color: rgba(0, 0, 0, 1)">,
</span>"dependencies"<span style="color: rgba(0, 0, 0, 1)">: {
</span>"react": "16.8.6"<span style="color: rgba(0, 0, 0, 1)">,
</span>"react-native": "0.60.4"<span style="color: rgba(0, 0, 0, 1)">
},
</span>"devDependencies"<span style="color: rgba(0, 0, 0, 1)">: {
</span>"@babel/core": "^7.5.5"<span style="color: rgba(0, 0, 0, 1)">,
</span>"@babel/runtime": "^7.5.5"<span style="color: rgba(0, 0, 0, 1)">,
</span>"@react-native-community/async-storage": "^1.6.1"<span style="color: rgba(0, 0, 0, 1)">,
</span>"@react-native-community/eslint-config": "^0.0.5"<span style="color: rgba(0, 0, 0, 1)">,
</span>"babel-jest": "^24.8.0"<span style="color: rgba(0, 0, 0, 1)">,
</span>"eslint": "^6.1.0"<span style="color: rgba(0, 0, 0, 1)">,
</span>"jest": "^24.8.0"<span style="color: rgba(0, 0, 0, 1)">,
</span>"metro-react-native-babel-preset": "^0.55.0"<span style="color: rgba(0, 0, 0, 1)">,
</span>"react-native-gesture-handler": "^1.3.0"<span style="color: rgba(0, 0, 0, 1)">,
</span>"react-native-image-picker": "^1.0.2"<span style="color: rgba(0, 0, 0, 1)">,
</span>"react-native-swiper": "^1.5.14"<span style="color: rgba(0, 0, 0, 1)">,
</span>"react-navigation": "^3.11.1"<span style="color: rgba(0, 0, 0, 1)">,
</span>"react-redux": "^7.1.0"<span style="color: rgba(0, 0, 0, 1)">,
</span>"react-test-renderer": "16.8.6"<span style="color: rgba(0, 0, 0, 1)">,
</span>"redux": "^4.0.4"<span style="color: rgba(0, 0, 0, 1)">,
</span>"redux-thunk": "^2.3.0"<span style="color: rgba(0, 0, 0, 1)">
},
</span>"jest"<span style="color: rgba(0, 0, 0, 1)">: {
</span>"preset": "react-native"<span style="color: rgba(0, 0, 0, 1)">
}
}</span></pre>
</div>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020226604-1636401225.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020329292-576645984.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020337206-971510651.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020400853-1080167912.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020407817-1408915175.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020419228-902414874.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020443778-1400722731.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020505355-58622143.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020517165-922667486.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020536711-1169838086.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020550609-1803325661.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020609634-564843761.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020714477-1190976121.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020645007-2045524134.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020700887-1344354569.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901020706443-1812194592.png"></p>
<p><strong>◆ App全屏幕启动页splash模板</strong></p>
<p><span style="font-size: 12px">react-native如何全屏启动? 设置StatusBar顶部条背景为透明 translucent={true},并配合RN动画Animated</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* @desc 启动页面
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
import React, { Component } from </span>'react'<span style="color: rgba(0, 0, 0, 1)">
import { StatusBar, Animated, View, Text, Image } from </span>'react-native'<span style="color: rgba(0, 0, 0, 1)">
export </span><span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> class Splash extends Component{
constructor(props){
super(props)
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.state =<span style="color: rgba(0, 0, 0, 1)"> {
animFadeIn: </span><span style="color: rgba(0, 0, 255, 1)">new</span> Animated.Value(0<span style="color: rgba(0, 0, 0, 1)">),
animFadeOut: </span><span style="color: rgba(0, 0, 255, 1)">new</span> Animated.Value(1<span style="color: rgba(0, 0, 0, 1)">),
}
}
render(){
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
</span><Animated.View style={}>
<StatusBar backgroundColor='transparent' barStyle='light-content' translucent={<span style="color: rgba(0, 0, 255, 1)">true</span>} />
<View style={GStyle.flex1_a_j}>
<Image source={require('../assets/img/ic_default.jpg')} style={{borderRadius: 100, width: 100, height: 100}} />
</View>
<View style={}>
<Text style={{color: '#dbdbdb', fontSize: 12, textAlign: 'center',}}>RN-ChatRoom v1.0.0</Text>
</View>
</Animated.View>
<span style="color: rgba(0, 0, 0, 1)"> )
}
componentDidMount(){
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 判断是否登录</span>
storage.get('hasLogin', (err, object) =><span style="color: rgba(0, 0, 0, 1)"> {
setTimeout(() </span>=><span style="color: rgba(0, 0, 0, 1)"> {
Animated.timing(
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.state.animFadeOut, {duration: 300, toValue: 0<span style="color: rgba(0, 0, 0, 1)">}
).start(()</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>
util.navigationReset(<span style="color: rgba(0, 0, 255, 1)">this</span>.props.navigation, (!err && object && object.hasLogin) ? 'Index' : 'Login'<span style="color: rgba(0, 0, 0, 1)">)
})
}, </span>1500<span style="color: rgba(0, 0, 0, 1)">);
})
}
}</span></pre>
</div>
<p><strong>◆ RN本地存储技术async-storage</strong></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* @desc 本地存储函数
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
import AsyncStorage from </span>'@react-native-community/async-storage'<span style="color: rgba(0, 0, 0, 1)">
export </span><span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> class Storage{
static get(key, callback){
</span><span style="color: rgba(0, 0, 255, 1)">return</span> AsyncStorage.getItem(key, (err, object) =><span style="color: rgba(0, 0, 0, 1)"> {
callback(err, JSON.parse(object))
})
}
static set(key, data, callback){
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> AsyncStorage.setItem(key, JSON.stringify(data), callback)
}
static del(key){
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> AsyncStorage.removeItem(key)
}
static clear(){
AsyncStorage.clear()
}
}
global.storage </span>= Storage</pre>
</div>
<p><span style="font-size: 12px">声明全局global变量,只需在App.js页面一次引入、多个页面均可调用。</span></p>
<p><span style="font-size: 12px; color: rgba(0, 0, 0, 1); background-color: rgba(224, 224, 224, 1)">storage.set('hasLogin', { hasLogin: true })</span><br><span style="font-size: 12px; color: rgba(0, 0, 0, 1); background-color: rgba(224, 224, 224, 1)">storage.get('hasLogin', (err, object) => { ... })</span></p>
<p><strong>◆ App主页面模板及全局引入组件</strong></p>
<div class="cnblogs_code">
<pre>import React, { Fragment, Component } from 'react'<span style="color: rgba(0, 0, 0, 1)">
import { StatusBar } from </span>'react-native'
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 引入公共js</span>
import './src/utils/util'<span style="color: rgba(0, 0, 0, 1)">
import </span>'./src/utils/storage'
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 导入样式</span>
import './src/assets/css/common'
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 导入rnPop弹窗</span>
import './src/assets/js/rnPop/rnPop.js'
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 引入页面路由</span>
import PageRouter from './src/router'<span style="color: rgba(0, 0, 0, 1)">
class App extends Component{
render(){
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
</span><Fragment><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)"> <StatusBar backgroundColor={GStyle.headerBackgroundColor} barStyle='light-content' /> </span><span style="color: rgba(0, 128, 0, 1)">*/</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, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">}
</span><PageRouter />
<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><RNPop />
</Fragment>
<span style="color: rgba(0, 0, 0, 1)"> )
}
}
export </span><span style="color: rgba(0, 0, 255, 1)">default</span> App</pre>
</div>
<p><strong>◆ react-navigation页面导航器/地址路由、底部tabbar</strong></p>
<p><span style="font-size: 12px">由于react-navigation官方顶部导航器不能满足需求,如是自己封装了一个,功能效果有些类似微信导航。</span></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901023717223-800709554.png"></p>
<div class="cnblogs_code">
<pre>export <span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> class HeaderBar extends Component {
constructor(props){
super(props)
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.state =<span style="color: rgba(0, 0, 0, 1)"> {
searchInput: </span>''<span style="color: rgba(0, 0, 0, 1)">
}
}
render() {
</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* 更新
* @param { navigation | 页面导航 }
* @param { title | 标题 }
* @param { center | 标题是否居中 }
* @param { search | 是否显示搜索 }
* @param { headerRight | 右侧Icon按钮 }
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
let{ navigation, title, bg, center, search, headerRight } </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.props
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
</span><View style={GStyle.flex_col}>
<StatusBar backgroundColor={bg ? bg : GStyle.headerBackgroundColor} barStyle='light-content' translucent={<span style="color: rgba(0, 0, 255, 1)">true</span>} />
<View style={}><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><TouchableOpacity style={} activeOpacity={.5} onPress={<span style="color: rgba(0, 0, 255, 1)">this</span>.goBack}><Text style={}>&#xe63f;</Text></TouchableOpacity><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>!search && center ? <View style={GStyle.flex1} /> : null }
<span style="color: rgba(0, 0, 0, 1)"> {
search </span>?<span style="color: rgba(0, 0, 0, 1)">
(
</span><View style={}>
<TextInput onChangeText={text=>{<span style="color: rgba(0, 0, 255, 1)">this</span>.setState({searchInput: text})}} style={styles.barSearchText} placeholder='搜索' placeholderTextColor='rgba(255,255,255,.6)' />
</View>
<span style="color: rgba(0, 0, 0, 1)"> )
:
(
</span><View style={}><span style="color: rgba(0, 0, 0, 1)">
{ title </span>? <Text style={}>{title}</Text> : null }
</View>
<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><View style={}><span style="color: rgba(0, 0, 0, 1)">
{
</span>!headerRight ? <span style="color: rgba(0, 0, 255, 1)">null</span> : headerRight.map((item, index) =><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><TouchableOpacity style={} activeOpacity={.5} key={index} onPress={()=>item.press ? item.press(<span style="color: rgba(0, 0, 255, 1)">this</span>.state.searchInput) : <span style="color: rgba(0, 0, 255, 1)">null</span>}><span style="color: rgba(0, 0, 0, 1)">
{
item.type </span>=== 'iconfont' ?<span style="color: rgba(0, 0, 0, 1)"> item.title : (
</span><span style="color: rgba(0, 0, 255, 1)">typeof</span> item.title === 'string' ?
<Text style={item.style ? item.style : <span style="color: rgba(0, 0, 255, 1)">null</span>}>{`${item.title}`}</Text>
<span style="color: rgba(0, 0, 0, 1)"> :
</span><Image source={item.title} style={{width: 24, height: 24, resizeMode: 'contain'}} />
<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)">}
{ item.badge </span>? <View style={}><Text style={GStyle.badge_text}>{item.badge}</Text></View> : <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)"> }
{ item.badgeDot </span>? <View style={}></View> : null }
</TouchableOpacity>
<span style="color: rgba(0, 0, 0, 1)"> )
})
}
</span></View>
</View>
</View>
<span style="color: rgba(0, 0, 0, 1)"> )
}
goBack </span>= () =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.props.navigation.goBack()
}
}</span></pre>
</div>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 创建底部TabBar</span>
const tabNavigator =<span style="color: rgba(0, 0, 0, 1)"> createBottomTabNavigator(
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> tabbar路由(消息、通讯录、我)</span>
<span style="color: rgba(0, 0, 0, 1)"> {
Index: {
screen: Index,
navigationOptions: ({navigation}) </span>=><span style="color: rgba(0, 0, 0, 1)"> ({
tabBarLabel: </span>'消息'<span style="color: rgba(0, 0, 0, 1)">,
tabBarIcon: ({focused, tintColor}) </span>=><span style="color: rgba(0, 0, 0, 1)"> (
</span><View>
<Text style={[ GStyle.iconfont, GStyle.fs_20, {color: (focused ? tintColor : '#999')} ]}>&#xe642;</Text>
<View style={}><Text style={GStyle.badge_text}>12</Text></View>
</View>
<span style="color: rgba(0, 0, 0, 1)"> )
})
},
Contact: {
screen: Contact,
navigationOptions: {
tabBarLabel: </span>'通讯录'<span style="color: rgba(0, 0, 0, 1)">,
tabBarIcon: ({focused, tintColor}) </span>=><span style="color: rgba(0, 0, 0, 1)"> (
</span><View>
<Text style={[ GStyle.iconfont, GStyle.fs_20, {color: (focused ? tintColor : '#999')} ]}>&#xe640;</Text>
</View>
<span style="color: rgba(0, 0, 0, 1)"> )
}
},
Ucenter: {
screen: Ucenter,
navigationOptions: {
tabBarLabel: </span>'我'<span style="color: rgba(0, 0, 0, 1)">,
tabBarIcon: ({focused, tintColor}) </span>=><span style="color: rgba(0, 0, 0, 1)"> (
</span><View>
<Text style={[ GStyle.iconfont, GStyle.fs_20, {color: (focused ? tintColor : '#999')} ]}>&#xe61e;</Text>
<View style={}></View>
</View>
<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)"> tabbar配置</span>
<span style="color: rgba(0, 0, 0, 1)"> {
...
}
)</span></pre>
</div>
<p><strong>◆ RN聊天页面功能模块</strong></p>
<p><span style="font-size: 12px">1、表情处理:原本是想着使用图片表情gif,可是在RN里面textInput文本框不能插入图片,只能通过定义一些特殊字符 :66: (:12 [奋斗] 解析表情,处理起来有些麻烦,而且图片多了影响性能,如是就改用emoj表情符。</span></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901024905606-200901246.png"> <img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901024942836-332295079.png"></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">faceList: [
{
nodes: [
</span>'🙂','😁','😋','😎','😍','😘','😗'<span style="color: rgba(0, 0, 0, 1)">,
</span>'😃','😂','🤣','😅','😉','😊','🤗'<span style="color: rgba(0, 0, 0, 1)">,
</span>'🤔','😐','😑','😶','🙄','😏','del'<span style="color: rgba(0, 0, 0, 1)">,
]
},
...
{
nodes: [
</span>'👓','👄','💋','👕','👙','👜','👠'<span style="color: rgba(0, 0, 0, 1)">,
</span>'👑','🎓','💄','💍','🌂','👧🏼','👨🏼'<span style="color: rgba(0, 0, 0, 1)">,
</span>'👵🏻','👴🏻','👨🌾','👨🏼🍳','👩🏻🍳','👨🏽✈️','del'<span style="color: rgba(0, 0, 0, 1)">,
]
},
...
]</span></pre>
</div>
<p><span style="font-size: 12px">2、光标定位:在指定光标处插入内容,textInput提供了光标起始位置</span></p>
<p><span style="font-size: 12px; background-color: rgba(224, 224, 224, 1)">let selection = this.textInput._lastNativeSelection || null;</span><br><span style="font-size: 12px; background-color: rgba(224, 224, 224, 1)">this.textInput.setNativeProps({</span><br><span style="font-size: 12px; background-color: rgba(224, 224, 224, 1)"> selection : { start : xxx, end : xxx}</span><br><span style="font-size: 12px; background-color: rgba(224, 224, 224, 1)">})</span></p>
<p><span style="font-size: 12px">3、textInput判断内容是否为空,过滤空格、回车</span></p>
<p><span style="font-size: 12px; background-color: rgba(224, 224, 224, 1)">isEmpty = (html) => {</span><br><span style="font-size: 12px; background-color: rgba(224, 224, 224, 1)"> return html.replace(/\r\n|\n|\r/, "").replace(/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g, "") == ""</span><br><span style="font-size: 12px; background-color: rgba(224, 224, 224, 1)">}</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* 聊天模块JS----------------------------------------------------
</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>
scrollToBottom = (t) =><span style="color: rgba(0, 0, 0, 1)"> {
let that </span>= <span style="color: rgba(0, 0, 255, 1)">this</span>
<span style="color: rgba(0, 0, 255, 1)">this</span>._timer = setTimeout(() =><span style="color: rgba(0, 0, 0, 1)"> {
that.refs.scrollView.scrollToEnd({animated: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">})
}, t </span>? 16 : 0<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>
hideKeyboard = () =><span style="color: rgba(0, 0, 0, 1)"> {
Keyboard </span>&&<span style="color: rgba(0, 0, 0, 1)"> Keyboard.dismiss()
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 点击表情</span>
handlePressEmotion = (img) =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">if</span>(img === 'del') <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">
let selection </span>= <span style="color: rgba(0, 0, 255, 1)">this</span>.editorInput._lastNativeSelection || <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 0, 1)">selection){
</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.setState({
editorText : </span><span style="color: rgba(0, 0, 255, 1)">this</span>.state.editorText +<span style="color: rgba(0, 0, 0, 1)"> `${img}`,
lastRange: </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.state.editorText.length
})
}
</span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
let startStr </span>= <span style="color: rgba(0, 0, 255, 1)">this</span>.state.editorText.substr(0 , <span style="color: rgba(0, 0, 255, 1)">this</span>.state.lastRange ? <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.state.lastRange : selection.start)
let endStr </span>= <span style="color: rgba(0, 0, 255, 1)">this</span>.state.editorText.substr(<span style="color: rgba(0, 0, 255, 1)">this</span>.state.lastRange ? <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.state.lastRange : selection.end)
</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.setState({
editorText : startStr </span>+ `${img}` +<span style="color: rgba(0, 0, 0, 1)"> endStr,
lastRange: (startStr </span>+<span style="color: rgba(0, 0, 0, 1)"> `${img}`).length
})
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 发送消息</span>
isEmpty = (html) =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">return</span> html.replace(/\r\n|\n|\r/, "").replace(/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g, "") == ""<span style="color: rgba(0, 0, 0, 1)">
}
handleSubmit </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, 255, 1)">if</span>(<span style="color: rgba(0, 0, 255, 1)">this</span>.isEmpty(<span style="color: rgba(0, 0, 255, 1)">this</span>.state.editorText)) <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">
let _msg </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.state.__messageJson
let _len </span>=<span style="color: rgba(0, 0, 0, 1)"> _msg.length
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 消息队列</span>
let _data =<span style="color: rgba(0, 0, 0, 1)"> {
id: `msg${</span>++<span style="color: rgba(0, 0, 0, 1)">_len}`,
msgtype: </span>3<span style="color: rgba(0, 0, 0, 1)">,
isme: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
avator: require(</span>'../../../assets/img/uimg/u__chat_img11.jpg'<span style="color: rgba(0, 0, 0, 1)">),
author: </span>'王梅(Fine)'<span style="color: rgba(0, 0, 0, 1)">,
msg: </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.state.editorText,
imgsrc: </span>''<span style="color: rgba(0, 0, 0, 1)">,
videosrc: </span>''<span style="color: rgba(0, 0, 0, 1)">
}
_msg </span>=<span style="color: rgba(0, 0, 0, 1)"> _msg.concat(_data)
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.setState({ __messageJson: _msg, editorText: ''<span style="color: rgba(0, 0, 0, 1)"> })
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.scrollToBottom(<span style="color: rgba(0, 0, 255, 1)">true</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, 128, 0, 1)">
//</span><span style="color: rgba(0, 128, 0, 1)"> 选择图片</span>
handleLaunchImage = () =><span style="color: rgba(0, 0, 0, 1)"> {
let that </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">
ImagePicker.launchImageLibrary({
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> title: '请选择图片来源',</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> cancelButtonTitle: '取消',</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> takePhotoButtonTitle: '拍照',</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> chooseFromLibraryButtonTitle: '相册图片',</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> customButtons: [</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> {name: 'baidu', title: 'baidu.com图片'},</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, 128, 0, 1)"> cameraType: 'back',</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> mediaType: 'photo',</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> videoQuality: 'high',</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> maxWidth: 300,</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> maxHeight: 300,</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> quality: .8,</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> noData: true,</span>
<span style="color: rgba(0, 0, 0, 1)"> storageOptions: {
skipBackup: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
},
}, (response) </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)"> console.log(response)</span>
<span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)">(response.didCancel){
console.log(</span>'user cancelled'<span style="color: rgba(0, 0, 0, 1)">)
}</span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)">(response.error){
console.log(</span>'ImagePicker Error'<span style="color: rgba(0, 0, 0, 1)">)
}</span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)">{
let source </span>=<span style="color: rgba(0, 0, 0, 1)"> { uri: response.uri }
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> let source = {uri: 'data:image/jpeg;base64,' + response.data}</span>
<span style="color: rgba(0, 0, 0, 1)"> that.setState({ imgsrc: source })
that.scrollToBottom(</span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">)
}
})
}</span></pre>
</div>
<p><strong>◆ 附上vue/react/angular聊天室IM实例</strong></p>
<p><span style="font-size: 12px; font-family: "comic sans ms", sans-serif">Vue网页版聊天室:https://www.cnblogs.com/xiaoyan2017/p/10793728.html<br></span></p>
<p><span style="font-size: 12px; font-family: "comic sans ms", sans-serif">React版聊天室:https://www.cnblogs.com/xiaoyan2017/p/11106246.html<br></span></p>
<p><span style="font-size: 12px; font-family: "comic sans ms", sans-serif">Angular聊天室:https://www.cnblogs.com/xiaoyan2017/p/11194828.html</span></p>
<p><img src="https://img2018.cnblogs.com/blog/1289798/201909/1289798-20190901031003881-1407468291.jpg"></p>
<p> </p>
</div>
<div id="MySignature" role="contentinfo">
本文为博主原创文章,未经博主允许不得转载,欢迎大家一起交流 QQ(282310962) wx(xy190310)<br><br>
来源:https://www.cnblogs.com/xiaoyan2017/p/11441285.html
頁:
[1]