经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » HTML/CSS » HTML5 » 查看文章
react-native聊天室|RN版聊天App仿微信实例|RN仿微信界面
来源:cnblogs  作者:xiaoyan2017  时间:2019/9/2 9:25:59  对本文有异议

一、前言

9月,又到开学的季节。为每个一直默默努力的自己点赞!最近都沉浸在react native原生app开发中,之前也有使用vue/react/angular等技术开发过聊天室项目,另外还使用RN技术做了个自定义模态弹窗rnPop组件。

一、项目简述

基于react+react-native+react-navigation+react-redux+react-native-swiper+rnPop等技术开发的仿微信原生App界面聊天室——RN_ChatRoom,实现了原生app启动页、AsyncStorage本地存储登录拦截、集成rnPop模态框功能(仿微信popupWindow弹窗菜单)、消息触摸列表、发送消息、表情(动图),图片预览,拍摄图片、发红包、仿微信朋友圈等功能。

二、技术点

  • MVVM框架:react / react-native / react-native-cli
  • 状态管理:react-redux / redux
  • 页面导航:react-navigation
  • rn弹窗组件:rnPop
  • 打包工具:webpack 2.0
  • 轮播组件:react-native-swiper
  • 图片/相册:react-native-image-picker
  1. {
  2. "name": "RN_ChatRoom",
  3. "version": "0.0.1",
  4. "aboutMe": "QQ:282310962 、 wx:xy190310",
  5. "dependencies": {
  6. "react": "16.8.6",
  7. "react-native": "0.60.4"
  8. },
  9. "devDependencies": {
  10. "@babel/core": "^7.5.5",
  11. "@babel/runtime": "^7.5.5",
  12. "@react-native-community/async-storage": "^1.6.1",
  13. "@react-native-community/eslint-config": "^0.0.5",
  14. "babel-jest": "^24.8.0",
  15. "eslint": "^6.1.0",
  16. "jest": "^24.8.0",
  17. "metro-react-native-babel-preset": "^0.55.0",
  18. "react-native-gesture-handler": "^1.3.0",
  19. "react-native-image-picker": "^1.0.2",
  20. "react-native-swiper": "^1.5.14",
  21. "react-navigation": "^3.11.1",
  22. "react-redux": "^7.1.0",
  23. "react-test-renderer": "16.8.6",
  24. "redux": "^4.0.4",
  25. "redux-thunk": "^2.3.0"
  26. },
  27. "jest": {
  28. "preset": "react-native"
  29. }
  30. }

◆ App全屏幕启动页splash模板

react-native如何全屏启动? 设置StatusBar顶部条背景为透明 translucent={true},并配合RN动画Animated

  1. /**
  2. * @desc 启动页面
  3. */
  4. import React, { Component } from 'react'
  5. import { StatusBar, Animated, View, Text, Image } from 'react-native'
  6. export default class Splash extends Component{
  7. constructor(props){
  8. super(props)
  9. this.state = {
  10. animFadeIn: new Animated.Value(0),
  11. animFadeOut: new Animated.Value(1),
  12. }
  13. }
  14. render(){
  15. return (
  16. <Animated.View style={[GStyle.flex1DC_a_j, {backgroundColor: '#1a4065', opacity: this.state.animFadeOut}]}>
  17. <StatusBar backgroundColor='transparent' barStyle='light-content' translucent={true} />
  18.  
  19. <View style={GStyle.flex1_a_j}>
  20. <Image source={require('../assets/img/ic_default.jpg')} style={{borderRadius: 100, width: 100, height: 100}} />
  21. </View>
  22. <View style={[GStyle.align_c, {paddingVertical: 20}]}>
  23. <Text style={{color: '#dbdbdb', fontSize: 12, textAlign: 'center',}}>RN-ChatRoom v1.0.0</Text>
  24. </View>
  25. </Animated.View>
  26. )
  27. }
  28. componentDidMount(){
  29. // 判断是否登录
  30. storage.get('hasLogin', (err, object) => {
  31. setTimeout(() => {
  32. Animated.timing(
  33. this.state.animFadeOut, {duration: 300, toValue: 0}
  34. ).start(()=>{
  35. // 跳转页面
  36. util.navigationReset(this.props.navigation, (!err && object && object.hasLogin) ? 'Index' : 'Login')
  37. })
  38. }, 1500);
  39. })
  40. }
  41. }

◆ RN本地存储技术async-storage

  1. /**
  2. * @desc 本地存储函数
  3. */
  4. import AsyncStorage from '@react-native-community/async-storage'
  5. export default class Storage{
  6. static get(key, callback){
  7. return AsyncStorage.getItem(key, (err, object) => {
  8. callback(err, JSON.parse(object))
  9. })
  10. }
  11. static set(key, data, callback){
  12. return AsyncStorage.setItem(key, JSON.stringify(data), callback)
  13. }
  14. static del(key){
  15. return AsyncStorage.removeItem(key)
  16. }
  17. static clear(){
  18. AsyncStorage.clear()
  19. }
  20. }
  21. global.storage = Storage

声明全局global变量,只需在App.js页面一次引入、多个页面均可调用。

storage.set('hasLogin', { hasLogin: true })
storage.get('hasLogin', (err, object) => { ... })

◆ App主页面模板及全局引入组件

  1. import React, { Fragment, Component } from 'react'
  2. import { StatusBar } from 'react-native'
  3.  
  4. // 引入公共js
  5. import './src/utils/util'
  6. import './src/utils/storage'
  7.  
  8. // 导入样式
  9. import './src/assets/css/common'
  10. // 导入rnPop弹窗
  11. import './src/assets/js/rnPop/rnPop.js'
  12.  
  13. // 引入页面路由
  14. import PageRouter from './src/router'
  15. class App extends Component{
  16. render(){
  17. return (
  18. <Fragment>
  19. {/* <StatusBar backgroundColor={GStyle.headerBackgroundColor} barStyle='light-content' /> */}
  20. {/* 页面 */}
  21. <PageRouter />
  22. {/* 弹窗模板 */}
  23. <RNPop />
  24. </Fragment>
  25. )
  26. }
  27. }
  28. export default App

◆ react-navigation页面导航器/地址路由、底部tabbar

由于react-navigation官方顶部导航器不能满足需求,如是自己封装了一个,功能效果有些类似微信导航。

  1. export default class HeaderBar extends Component {
  2. constructor(props){
  3. super(props)
  4. this.state = {
  5. searchInput: ''
  6. }
  7. }
  8. render() {
  9. /**
  10. * 更新
  11. * @param { navigation | 页面导航 }
  12. * @param { title | 标题 }
  13. * @param { center | 标题是否居中 }
  14. * @param { search | 是否显示搜索 }
  15. * @param { headerRight | 右侧Icon按钮 }
  16. */
  17. let{ navigation, title, bg, center, search, headerRight } = this.props
  18. return (
  19. <View style={GStyle.flex_col}>
  20. <StatusBar backgroundColor={bg ? bg : GStyle.headerBackgroundColor} barStyle='light-content' translucent={true} />
  21. <View style={[styles.rnim__topBar, GStyle.flex_row, {backgroundColor: bg ? bg : GStyle.headerBackgroundColor}]}>
  22. {/* 返回 */}
  23. <TouchableOpacity style={[styles.iconBack]} activeOpacity={.5} onPress={this.goBack}><Text style={[GStyle.iconfont, GStyle.c_fff, GStyle.fs_18]}>&#xe63f;</Text></TouchableOpacity>
  24. {/* 标题 */}
  25. { !search && center ? <View style={GStyle.flex1} /> : null }
  26. {
  27. search ?
  28. (
  29. <View style={[styles.barSearch, GStyle.flex1, GStyle.flex_row]}>
  30. <TextInput onChangeText={text=>{this.setState({searchInput: text})}} style={styles.barSearchText} placeholder='搜索' placeholderTextColor='rgba(255,255,255,.6)' />
  31. </View>
  32. )
  33. :
  34. (
  35. <View style={[styles.barTit, GStyle.flex1, GStyle.flex_row, center ? styles.barTitCenter : null]}>
  36. { title ? <Text style={[styles.barCell, {fontSize: 16, paddingLeft: 0}]}>{title}</Text> : null }
  37. </View>
  38. )
  39. }
  40. {/* 右侧 */}
  41. <View style={[styles.barBtn, GStyle.flex_row]}>
  42. {
  43. !headerRight ? null : headerRight.map((item, index) => {
  44. return(
  45. <TouchableOpacity style={[styles.iconItem]} activeOpacity={.5} key={index} onPress={()=>item.press ? item.press(this.state.searchInput) : null}>
  46. {
  47. item.type === 'iconfont' ? item.title : (
  48. typeof item.title === 'string' ?
  49. <Text style={item.style ? item.style : null}>{`${item.title}`}</Text>
  50. :
  51. <Image source={item.title} style={{width: 24, height: 24, resizeMode: 'contain'}} />
  52. )
  53. }
  54. {/* 圆点 */}
  55. { item.badge ? <View style={[styles.iconBadge, GStyle.badge]}><Text style={GStyle.badge_text}>{item.badge}</Text></View> : null }
  56. { item.badgeDot ? <View style={[styles.iconBadgeDot, GStyle.badge_dot]}></View> : null }
  57. </TouchableOpacity>
  58. )
  59. })
  60. }
  61. </View>
  62. </View>
  63. </View>
  64. )
  65. }
  66. goBack = () => {
  67. this.props.navigation.goBack()
  68. }
  69. }
  1. // 创建底部TabBar
  2. const tabNavigator = createBottomTabNavigator(
  3. // tabbar路由(消息、通讯录、我)
  4. {
  5. Index: {
  6. screen: Index,
  7. navigationOptions: ({navigation}) => ({
  8. tabBarLabel: '消息',
  9. tabBarIcon: ({focused, tintColor}) => (
  10. <View>
  11. <Text style={[ GStyle.iconfont, GStyle.fs_20, {color: (focused ? tintColor : '#999')} ]}>&#xe642;</Text>
  12. <View style={[GStyle.badge, {position: 'absolute', top: -2, right: -15,}]}><Text style={GStyle.badge_text}>12</Text></View>
  13. </View>
  14. )
  15. })
  16. },
  17. Contact: {
  18. screen: Contact,
  19. navigationOptions: {
  20. tabBarLabel: '通讯录',
  21. tabBarIcon: ({focused, tintColor}) => (
  22. <View>
  23. <Text style={[ GStyle.iconfont, GStyle.fs_20, {color: (focused ? tintColor : '#999')} ]}>&#xe640;</Text>
  24. </View>
  25. )
  26. }
  27. },
  28. Ucenter: {
  29. screen: Ucenter,
  30. navigationOptions: {
  31. tabBarLabel: '我',
  32. tabBarIcon: ({focused, tintColor}) => (
  33. <View>
  34. <Text style={[ GStyle.iconfont, GStyle.fs_20, {color: (focused ? tintColor : '#999')} ]}>&#xe61e;</Text>
  35. <View style={[GStyle.badge_dot, {position: 'absolute', top: -2, right: -6,}]}></View>
  36. </View>
  37. )
  38. }
  39. }
  40. },
  41. // tabbar配置
  42. {
  43. ...
  44. }
  45. )

◆ RN聊天页面功能模块

1、表情处理:原本是想着使用图片表情gif,可是在RN里面textInput文本框不能插入图片,只能通过定义一些特殊字符 :66: (:12 [奋斗] 解析表情,处理起来有些麻烦,而且图片多了影响性能,如是就改用emoj表情符。

 

  1. faceList: [
  2. {
  3. nodes: [
  4. '??','??','??','??','??','??','??',
  5. '??','??','??','??','??','??','??',
  6. '??','??','??','??','??','??','del',
  7. ]
  8. },
  9. ...
  10. {
  11. nodes: [
  12. '??','??','??','??','??','??','??',
  13. '??','??','??','??','??','????','????',
  14. '????','????','??‍??','????‍??','????‍??','????‍??','del',
  15. ]
  16. },
  17. ...
  18. ]

2、光标定位:在指定光标处插入内容,textInput提供了光标起始位置

let selection = this.textInput._lastNativeSelection || null;
this.textInput.setNativeProps({
  selection : { start : xxx, end : xxx}
})

3、textInput判断内容是否为空,过滤空格、回车

isEmpty = (html) => {
  return html.replace(/\r\n|\n|\r/, "").replace(/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g, "") == ""
}

  1. /**
  2. * 聊天模块JS----------------------------------------------------
  3. */
  4. // ...滚动至聊天底部
  5. scrollToBottom = (t) => {
  6. let that = this
  7. this._timer = setTimeout(() => {
  8. that.refs.scrollView.scrollToEnd({animated: false})
  9. }, t ? 16 : 0);
  10. }
  11. // ...隐藏键盘
  12. hideKeyboard = () => {
  13. Keyboard && Keyboard.dismiss()
  14. }
  15. // 点击表情
  16. handlePressEmotion = (img) => {
  17. if(img === 'del') return
  18. let selection = this.editorInput._lastNativeSelection || null;
  19. if (!selection){
  20. this.setState({
  21. editorText : this.state.editorText + `${img}`,
  22. lastRange: this.state.editorText.length
  23. })
  24. }
  25. else {
  26. let startStr = this.state.editorText.substr(0 , this.state.lastRange ? this.state.lastRange : selection.start)
  27. let endStr = this.state.editorText.substr(this.state.lastRange ? this.state.lastRange : selection.end)
  28. this.setState({
  29. editorText : startStr + `${img}` + endStr,
  30. lastRange: (startStr + `${img}`).length
  31. })
  32. }
  33. }
  34. // 发送消息
  35. isEmpty = (html) => {
  36. return html.replace(/\r\n|\n|\r/, "").replace(/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g, "") == ""
  37. }
  38. handleSubmit = () => {
  39. // 判断是否为空值
  40. if(this.isEmpty(this.state.editorText)) return
  41. let _msg = this.state.__messageJson
  42. let _len = _msg.length
  43. // 消息队列
  44. let _data = {
  45. id: `msg${++_len}`,
  46. msgtype: 3,
  47. isme: true,
  48. avator: require('../../../assets/img/uimg/u__chat_img11.jpg'),
  49. author: '王梅(Fine)',
  50. msg: this.state.editorText,
  51. imgsrc: '',
  52. videosrc: ''
  53. }
  54. _msg = _msg.concat(_data)
  55. this.setState({ __messageJson: _msg, editorText: '' })
  56. this.scrollToBottom(true)
  57. }
  58. // >>> 【选择区功能模块】------------------------------------------
  59. // 选择图片
  60. handleLaunchImage = () => {
  61. let that = this
  62. ImagePicker.launchImageLibrary({
  63. // title: '请选择图片来源',
  64. // cancelButtonTitle: '取消',
  65. // takePhotoButtonTitle: '拍照',
  66. // chooseFromLibraryButtonTitle: '相册图片',
  67. // customButtons: [
  68. // {name: 'baidu', title: 'baidu.com图片'},
  69. // ],
  70. // cameraType: 'back',
  71. // mediaType: 'photo',
  72. // videoQuality: 'high',
  73. // maxWidth: 300,
  74. // maxHeight: 300,
  75. // quality: .8,
  76. // noData: true,
  77. storageOptions: {
  78. skipBackup: true,
  79. },
  80. }, (response) => {
  81. // console.log(response)
  82.  
  83. if(response.didCancel){
  84. console.log('user cancelled')
  85. }else if(response.error){
  86. console.log('ImagePicker Error')
  87. }else{
  88. let source = { uri: response.uri }
  89. // let source = {uri: 'data:image/jpeg;base64,' + response.data}
  90. that.setState({ imgsrc: source })
  91. that.scrollToBottom(true)
  92. }
  93. })
  94. }

◆ 附上vue/react/angular聊天室IM实例

Vue网页版聊天室:https://www.cnblogs.com/xiaoyan2017/p/10793728.html

React版聊天室:https://www.cnblogs.com/xiaoyan2017/p/11106246.html

Angular聊天室:https://www.cnblogs.com/xiaoyan2017/p/11194828.html

 

原文链接:http://www.cnblogs.com/xiaoyan2017/p/11441285.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号