taro-chatroom多端实战项目是基于taro react redux reactNative等技术开发的仿微信界面聊天实例,实现了消息发送、动图表情、图片查看、红包/朋友圈、小组等功能。
支持编译到多端:h5 小程序 app端 ,效果图如下:
技术实现:
- 编码/技术:VScode react/taro/redux/reactNative
- iconfont图标:阿里字体图标库
- 自定义导航栏Navigation 底部Tabbar
- 弹窗组件:taroPop(基于Taro封装自定义模态框)
- 支持编译:H5端 小程序 app端
页面入口app.jsx配置
代码语言:javascript复制/**
* @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'))
taro中通过如下方式配置页面路径及全局窗口配置
代码语言:javascript复制config = {
pages: [
'pages/auth/login/index',
'pages/auth/register/index',
'pages/index/index',
...
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'TaroChat',
navigationBarTextStyle: 'black',
navigationStyle: 'custom'
}
}
项目中顶部导航条及底部tabbar均为自定义组件模式,这里不多介绍,可以去看之前的分享文章
Taro实现自定义导航栏 Tabbar菜单
Taro仿ios/android对话框|模态框
taro实现登录表单验证、状态管理、本地存储
代码语言:javascript复制<Input placeholder="请输入手机号/昵称" onInput={this.handleInput.bind(this, 'tel')} />
代码语言:javascript复制this.state = {
tel: '',
pwd: '',
}
handleInput = (key, e) => {
this.setState({ [key]: e.detail.value })
}
通过上面方法就可以简单实现获取input值了。更详细介绍如下:
代码语言:javascript复制<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>
代码语言:javascript复制/**
* @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)
需要注意的是 taro中编译到RN端 不支持同步存储setStorage,只能使用setStorageSync异步存储了
对于一些兼容样式,不编译到RN端,则可通过如下代码包裹实现
/*postcss-pxtransform rn eject enable*/
/*postcss-pxtransform rn eject disable*/
taro滚动聊天消息底部
在taro中实现聊天消息滚动到底部也需要兼容处理,由于RN端不支持 createSelectorQuery
代码语言:javascript复制// 滚动至聊天底部
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)
}
代码语言:javascript复制componentDidMount() {
if(process.env.TARO_ENV === 'rn') {
this.scrollMsgBottomRN()
}else {
this.scrollMsgBottom()
}
}
taro提供环境变量来支持不同平台的兼容性
代码语言:javascript复制let taroEnv = process.env.TARO_ENV
if(taroEnv === 'rn') {
...
}else if(taroEnv === 'weapp') {
...
}else if(taroEnv === 'h5') {
...
}
代码语言:javascript复制// 渲染消息记录
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>
))
}
代码语言:javascript复制...
// 点击聊天消息区域
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聊天实例项目到这里就介绍完了,希望能有些帮助!!