taro聊天实例|react+taro仿微信App聊天界面

日志狂
• 阅读 6089

项目介绍

taro-chatroom仿微信聊天室项目是基于taro+react+react-redux+ReactNative+taroPop等技术实现的taro版聊天App实例,支持编译到三端h5+小程序+RN端,实现了消息发送、表情大图,图片预览、长按菜单、红包、朋友圈等功能。

如下图:编译到多端效果:H5端/小程序/App端
taro聊天实例|react+taro仿微信App聊天界面

技术实现:

  • 编码/技术:Vscode + react/taro/redux/react-native
  • iconfont图标:阿里字体图标库
  • 自定义导航栏Navigation + 底部Tabbar
  • 弹窗组件:taroPop(基于Taro封装自定义模态框)
  • 支持编译:H5端 + 小程序 + RN端

taro聊天实例|react+taro仿微信App聊天界面

taro聊天实例|react+taro仿微信App聊天界面

taro聊天实例|react+taro仿微信App聊天界面

taro聊天实例|react+taro仿微信App聊天界面

taro聊天实例|react+taro仿微信App聊天界面

taro聊天实例|react+taro仿微信App聊天界面

taro聊天实例|react+taro仿微信App聊天界面

taro聊天实例|react+taro仿微信App聊天界面

taro聊天实例|react+taro仿微信App聊天界面

taro聊天实例|react+taro仿微信App聊天界面

taro聊天实例|react+taro仿微信App聊天界面

taro聊天实例|react+taro仿微信App聊天界面

taro聊天实例|react+taro仿微信App聊天界面

taro聊天实例|react+taro仿微信App聊天界面

引入公共样式及状态管理

/**
  * @desc   Taro入口页面 app.jsx
  * @about  Q:282310962  wx:xy190310
  */

import Taro, { Component } from '@tarojs/taro'
import Index from './pages/index'

// 引入状态管理redux
import { Provider } from '@tarojs/redux'
import { store } from './store'

// 引入样式
import './app.scss'
import './styles/fonts/iconfont.css'
import './styles/reset.scss'

class App extends Component {
  config = {
    pages: [
      'pages/auth/login/index',
      'pages/auth/register/index',
      'pages/index/index',
      ...
    ],
    window: {
      backgroundTextStyle: 'light',
      navigationBarBackgroundColor: '#fff',
      navigationBarTitleText: 'TaroChat',
      navigationBarTextStyle: 'black',
      navigationStyle: 'custom'
    }
  }
  
  // 在 App 类中的 render() 函数没有实际作用
  // 请勿修改此函数
  render () {
    return (
      <Provider store={store}>
        <Index />
      </Provider>
    )
  }
}

Taro.render(<App />, document.getElementById('app'))

顶部导航栏/底部tabbar均为自定义组件,详情见:
Taro实现自定义导航栏+Tabbar菜单
弹窗插件是基于Taro自定义模态框组件,参看:
Taro仿ios/android对话框|模态框

taro表单验证|状态管理|本地存储

return (
    <View className="taro__container flexDC bg-eef1f5">
        <Navigation background='#eef1f5' fixed />
        
        <ScrollView className="taro__scrollview flex1" scrollY>
            <View className="auth-lgreg">
                {/* logo */}
                <View className="auth-lgreg__slogan">
                    <View className="auth-lgreg__slogan-logo">
                        <Image className="auth-lgreg__slogan-logo__img" src={require('../../../assets/taro.png')} mode="aspectFit" />
                    </View>
                    <Text className="auth-lgreg__slogan-text">欢迎来到Taro-Chatroom</Text>
                </View>
                {/* 表单 */}
                <View className="auth-lgreg__forms">
                    <View className="auth-lgreg__forms-wrap">
                        <View className="auth-lgreg__forms-item">
                            <Input className="auth-lgreg__forms-iptxt flex1" placeholder="请输入手机号/昵称" onInput={this.handleInput.bind(this, 'tel')} />
                        </View>
                        <View className="auth-lgreg__forms-item">
                            <Input className="auth-lgreg__forms-iptxt flex1" placeholder="请输入密码" password onInput={this.handleInput.bind(this, 'pwd')} />
                        </View>
                    </View>
                    <View className="auth-lgreg__forms-action">
                        <TouchView onClick={this.handleSubmit}><Text className="auth-lgreg__forms-action__btn">登录</Text></TouchView>
                    </View>
                    <View className="auth-lgreg__forms-link">
                        <Text className="auth-lgreg__forms-link__nav">忘记密码</Text>
                        <Text className="auth-lgreg__forms-link__nav" onClick={this.GoToRegister}>注册账号</Text>
                    </View>
                </View>
            </View>
        </ScrollView>

        <TaroPop ref="taroPop" />
    </View>
)

由于taro中ReactNative端不支持同步存储,只能使用异步存储实现
taro聊天实例|react+taro仿微信App聊天界面

/**
 * @tpl 登录模块
 */

import Taro from '@tarojs/taro'
import { View, Text, ScrollView, Image, Input, Button } from '@tarojs/components'

import './index.scss'

import { connect } from '@tarojs/redux'
import * as actions from '../../../store/action'...

class Login extends Taro.Component {
    config = {
        navigationBarTitleText: '登录'
    }
    constructor(props) {
        super(props)
        this.state = {
            tel: '',
            pwd: '',
        }
    }
    componentWillMount() {
        // 判断是否登录
        storage.get('hasLogin').then(res => {
            if(res && res.hasLogin) {
                Taro.navigateTo({url: '/pages/index/index'})
            }
        })
    }
    // 提交表单
    handleSubmit = () => {
        let taroPop = this.refs.taroPop
        let { tel, pwd } = this.state

        if(!tel) {
            taroPop.show({content: '手机号不能为空', time: 2})
        }else if(!util.checkTel(tel)) {
            taroPop.show({content: '手机号格式有误', time: 2})
        }else if(!pwd) {
            taroPop.show({content: '密码不能为空', time: 2})
        }else {
            // ...接口数据
            ...
            
            storage.set('hasLogin', { hasLogin: true })
            storage.set('user', { username: tel })
            storage.set('token', { token: util.setToken() })

            taroPop.show({
                skin: 'toast',
                content: '登录成功',
                icon: 'success',
                time: 2
            })
            
            ...
        }
    }
    
    render () {
        ...
    }
}

const mapStateToProps = (state) => {
    return {...state.auth}
}

export default connect(mapStateToProps, {
    ...actions
})(Login)
import Taro from '@tarojs/taro'

export default class Storage {
    static get(key) {
        return Taro.getStorage({ key }).then(res => res.data).catch(() => '')
    }

    static set(key, data){
        return Taro.setStorage({key: key, data: data}).then(res => res)
    }

    ...
}

对于一些兼容样式,不编译到RN端,则可通过如下代码包裹实现
/*postcss-pxtransform rn eject enable*/
/*postcss-pxtransform rn eject disable*/

taro滚动聊天至最底部

taro中实现聊天消息滚动到最底部,由于RN端不支持 createSelectorQuery,需要做兼容处理。

componentDidMount() {
    if(process.env.TARO_ENV === 'rn') {
        this.scrollMsgBottomRN()
    }else {
        this.scrollMsgBottom()
    }
}
// 滚动至聊天底部
scrollMsgBottom = () => {
    let query = Taro.createSelectorQuery()
    query.select('#scrollview').boundingClientRect()
    query.select('#msglistview').boundingClientRect()
    query.exec((res) => {
        // console.log(res)
        if(res[1].height > res[0].height) {
            this.setState({ scrollTop: res[1].height - res[0].height })
        }
    })
}
scrollMsgBottomRN = (t) => {
    let that = this
    this._timer = setTimeout(() => {
        that.refs.ScrollViewRN.scrollToEnd({animated: false})
    }, t ? 16 : 0)
}

另外表情部分,则是使用emoj表情符,实现比较简单,就不介绍了。
taro聊天实例|react+taro仿微信App聊天界面

// 渲染消息记录
renderMsgTpl = (data) => {
    return data.map((item, index) => (
        <View key={index}>
            {item.msgtype == 1 && 
            <View className="msgitem msg__time"><Text className="msg__text">{item.msg}</Text></View>
            }
            
            {item.msgtype == 2 && 
            <View className="msgitem msg__notice"><Text className="msg__text">{item.msg}</Text></View>
            }
            
            {item.msgtype == 3 && 
            <View className="msgitem">
                {!item.isme ? <View className="msg__avator"><Image className="msg__avator-img" src={item.avator} mode="aspectFit" /></View> : null}
                <View className={`msg__cntbox ${item.isme ? 'msg-me' : 'msg-others'}`}>
                    <Text className="msg-author">{item.author}</Text>
                    <View className={`msg__cnt ${item.isme ? 'msg__cnt-me' : 'msg__cnt-others'}`} onLongPress={this.handleLongPressMenu}>
                        <Text className="msg__cnt-text">{item.msg}</Text>
                    </View>
                </View>
                {item.isme ? <View className="msg__avator"><Image className="msg__avator-img" src={item.avator} mode="aspectFit" /></View> : null}
            </View>
            }
            
            {item.msgtype == 4 && 
            <View className="msgitem">
                {!item.isme ? <View className="msg__avator"><Image className="msg__avator-img" src={item.avator} mode="aspectFit" /></View> : null}
                <View className={`msg__cntbox ${item.isme ? 'msg-me' : 'msg-others'}`}>
                    <Text className="msg-author">{item.author}</Text>
                    <View className={`msg__cnt ${item.isme ? 'msg__cnt-me' : 'msg__cnt-others'} msg__lgface`} onLongPress={this.handleLongPressMenu}>
                        <Image className="msg__lgface-img" src={item.imgsrc} mode="widthFix" />
                    </View>
                </View>
                {item.isme ? <View className="msg__avator"><Image className="msg__avator-img" src={item.avator} mode="aspectFit" /></View> : null}
            </View>
            }
            
            {item.msgtype == 5 && 
            <View className="msgitem">
                {!item.isme ? <View className="msg__avator"><Image className="msg__avator-img" src={item.avator} mode="aspectFit" /></View> : null}
                <View className={`msg__cntbox ${item.isme ? 'msg-me' : 'msg-others'}`}>
                    <Text className="msg-author">{item.author}</Text>
                    <View className={`msg__cnt ${item.isme ? 'msg__cnt-me' : 'msg__cnt-others'} msg__picture`} onClick={this.handlePreviewPicture.bind(this, item.imgsrc)} onLongPress={this.handleLongPressMenu}>
                        <Image className="msg__picture-img" src={item.imgsrc} mode="widthFix" />
                    </View>
                </View>
                {item.isme ? <View className="msg__avator"><Image className="msg__avator-img" src={item.avator} mode="aspectFit" /></View> : null}
            </View>
            }
            
            ...
        </View>
    ))
}
...

// 点击聊天消息区域
msgPanelClicked = () => {
    if(!this.state.showFootToolbar) return
    this.setState({ showFootToolbar: false })
}

// 表情、选择区切换
swtEmojChooseView = (index) => {
    this.setState({ showFootToolbar: true, showFootViewIndex: index })
}

// 底部表情tab切换
swtEmojTab = (index) => {
    let lists = this.state.emotionJson
    for(var i = 0, len = lists.length; i < len; i++) {
        lists[i].selected = false
    }
    lists[index].selected = true
    this.setState({ emotionJson: lists })
}


/* >>> 【编辑器/表情处理模块】------------------------------------- */
bindEditorInput = (e) => {
    this.setState({
        editorText: e.detail.value,
        editorLastCursor: e.detail.cursor
    })
}
bindEditorFocus = (e) => {
    this.setState({ editorLastCursor: e.detail.cursor })
}
bindEditorBlur = (e) => {
    this.setState({ editorLastCursor: e.detail.cursor })
}

handleEmotionTaped = (emoj) => {
    if(emoj == 'del') return
    // 在光标处插入表情
    let { editorText, editorLastCursor } = this.state
    let lastCursor = editorLastCursor ? editorLastCursor : editorText.length
    let startStr = editorText.substr(0, lastCursor)
    let endStr = editorText.substr(lastCursor)
    this.setState({
        editorText: startStr + `${emoj} ` + endStr
    })
}

...

到这里taro开发聊天app就基本介绍完了,希望大家能喜欢~~
最后分享个基于Vue实例项目
vue+uniapp+vuex开发的仿抖音短视频|仿陌陌直播项目

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这