[React] 15 - Redux: practice IM

本篇屬於私人筆記。html

 

client 引導部分


1、assets: 音頻,圖片,字體

├── assets
│   ├── audios
│   ├── fonts
│   └── images

 

 

2、main"膠水"函數

├── App.jsx
├── App.less
├── main.js
└── templates
    └── index.html

 

  • 引導入口

templates/index.html 涉及到 webpack,另起一篇單述。【待定】react

module.exports = [
    {
        filename: 'index.html',
        template: path.resolve(__dirname, '../client/templates/index.html'),
        inject  : true,
        chunks  : ['app'],
        entry   : {
            key : 'app',
            file: path.resolve(__dirname, '../client/main.js'),
        },
    },
];

 

  • 引導順序

基本上:index.htmlapp--> main.js (膠水角色) --> App.js【UI組件】webpack

main.js

ReactDom.render(
<Provider store={store}> <App /> </Provider>, document.getElementById('app'), );

 

  • 初始化功能

異步通訊涉及到的socket.io部分。【待定】ios

 

 

 

client's Redux 部分


1、誰是第一個 container

  • connect 函數

沒有第二個參數:mapDispatchToPropsweb

以下可見,狀態有不少屬性。redux

[state/reducer.js]react-native

const initialState = immutable.fromJS({
    user: null,
    focus: '',
    connect: true,
    ui: {
        showLoginDialog: false,
        primaryColor,
        primaryTextColor,
        backgroundImage,
        sound,
        soundSwitch,
        notificationSwitch,
        voiceSwitch,
    },
});

Ref: immutable.js 在React、Redux中的實踐以及經常使用API簡介 【待定】設計模式

 

  • 獲取"可用"狀態

 (1). render中獲取狀態,用於顯示;app

 (2). componentDidMount 中獲取,用於顯示;框架

 

 

2、UI組件

├── modules
│   └── main
│       ├── Main.jsx
│       ├── Main.less

  

  • Main 組件

Login是獨立的Dialog,因此在此不展開。

main主鍵在這裏是一個childStyle,相似子窗口。

import Main from './modules/main/Main';
render() {
  const { showLoginDialog } = this.props;
  return (
    <div className="app" style={this.style}>
      <div className="blur"  style={this.blurStyle} />
      <div className="child" style={this.childStyle}>
        <Main />
      </div>
      <Dialog visible={showLoginDialog} closable={false} onClose={action.closeLoginDialog}>
        <Login />
      </Dialog>
    </div>
  );
}

 

  • UI 組件的組合
import React, { Component } from 'react';
import { immutableRenderDecorator } from 'react-immutable-render-mixin';

import Sidebar   from './sidebar/Sidebar';
import ChatPanel from './chatPanel/ChatPanel';
import './Main.less';

/**
* 能夠實現裝飾器的寫法
*/ @immutableRenderDecorator class Main extends Component { render() {
return ( <div className="module-main"> <Sidebar /> <ChatPanel /> </div> ); } } export default Main;

裝飾器模式(Decorator Pattern),容許向一個現有的對象添加新的功能,同時又不改變其結構。這種類型的設計模式屬於結構型模式,它是做爲現有的類的一個包裝。

 

 

3、嵌套下的"第二個 container"

├── modules
│   └── main
│       ├── chatPanel
│       └── sidebar
│           ├── AppDownload.jsx
│           ├── OnlineStatus.jsx
│           ├── Sidebar.jsx
│           ├── Sidebar.less
│           ├── SingleCheckButton.jsx
│           └── SingleCheckGroup.jsx

 

siderBar是個獨立的子模塊,等價於setting page。 

[sideBar/Sidebar.jsx]

 

 

4、另外兩個 container

[chatPanel/ChatPanel.jsx]

import React, { Component } from 'react';

import FeatureLinkmans from './featureLinkmans/FeatureLinkmans';
import Chat            from './chat/Chat';
import './ChatPanel.less';

class ChatPanel extends Component {
    render() {
        return (
            <div className="module-main-chatPanel">
                <FeatureLinkmans />       # 其中有一個connect
                <Chat />                  # 其中有一個connect
            </div>
        );
    }
}

export default ChatPanel;

[featureLinkmans/FeatureLinkmans.jsx]

export default connect(state => ({
    isLogin: !!state.getIn(['user', '_id']),
}))(FeatureLinkmans);

[chat/chat.js] 

export default connect((state) => {
    const isLogin = !!state.getIn(['user', '_id']);
    if (!isLogin) {
        return {
            userId : '',
            focus  : state.getIn(['user', 'linkmans', 0, '_id']),
            creator: '',
            avatar : state.getIn(['user', 'linkmans', 0, 'avatar']),
            members: state.getIn(['user', 'linkmans', 0, 'members']) || immutable.List(),
        };
    }

    const focus   = state.get('focus');
    const linkman = state.getIn(['user', 'linkmans']).find(g => g.get('_id') === focus);

    return {
        userId:  state.getIn(['user', '_id']),
        focus,
        type:    linkman.get('type'),
        creator: linkman.get('creator'),
        to:      linkman.get('to'),
        name:    linkman.get('name'),
        avatar:  linkman.get('avatar'),
        members: linkman.get('members') || immutable.fromJS([]),
    };
})(Chat);

 

固然,以後嵌套的connect以及contrainer還有不少。

 

 

5、動做信號

├── state
│   ├── action.js
│   ├── reducer.js
│   └── store.js

 

  • createStore 接收 reducer
import { createStore } from 'redux';
import reducer from './reducer';

const store = createStore(
    reducer,
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
);
export
default store;

 

  • reducer 返回新狀態
const initialState = immutable.fromJS({
    user   : null,
    focus  : '',
    connect: true,
    ui: {
        showLoginDialog: false,
        primaryColor,
        primaryTextColor,
        backgroundImage,
        sound,
        soundSwitch,
        notificationSwitch,
        voiceSwitch,
    },
});

這裏採用了switch的方式判斷:action.type。

function reducer(state = initialState, action) {
    switch (action.type) {
    case 'Logout': {
       ...return newState;
    }
    case 'SetDeepValue': {
        return newState;
    }

...

 

  • action 定義動做信號

 ----> 這裏有值得玩味的地方,以前connect沒有使用第二個參數,故,咱們仍然採用dispatch的「非自動方式」發送信號。

# 舉例子

async function setGuest(defaultGroup) { defaultGroup.messages.forEach(m => convertRobot10Message(m)); dispatch({ type: 'SetDeepValue', keys: ['user'], value: { linkmans: [ Object.assign(defaultGroup, { type: 'group', unread: 0, members: [], }), ] }, }); }

 

----> 大括號自動處理動做信號。

下圖中的大括號{...}就有了默認執行dispatch的意思,即執行上圖return的動做信號。

 

 

 

React Native 移植


1、代碼結構

除了 App.js 以及utils文件夾外,還包括下面的主要移植內容。

.
├── App.js  <---- 實際的起始點
├── assets
│   └── images
│       ├── 0.jpg
│       └── baidu.png
├── components
│   ├── Avatar.js
│   ├── Expression.js
│   └── Image.js
├── pages
│   ├── Chat
│   │   ├── Chat.js
│   │   ├── Input.js
│   │   ├── Message.js
│   │   └── MessageList.js
│   ├── ChatList
│   │   ├── ChatList.js
│   │   └── Linkman.js
│   ├── LoginSignup
│   │   ├── Base.js
│   │   ├── Login.js
│   │   └── Signup.js
│   └── test.js
├── socket.js
└── state
    ├── action.js
    ├── reducer.js
    └── store.js

 

 

2、引導部分

Main的provider部分需改成router,由於手機只適合page router。

也就是,Main.js+App.js in Web ----> App.js in RN

import React from 'react';
import { StyleSheet, View, AsyncStorage, Alert } from 'react-native';
import { Provider }      from 'react-redux';
import { Scene, Router } from 'react-native-router-flux';
import PropTypes   from 'prop-types';
import { Root } from 'native-base';
import { Updates } from 'expo';

import socket from './socket';
import fetch  from '../utils/fetch';
import action from './state/action';
import store  from './state/store';
import convertRobot10Message from '../utils/convertRobot10Message';
import getFriendId from '../utils/getFriendId';
import platform from '../utils/platform'; import packageInfo from '../package';

// App控件中的內容也放在了這裏,成爲「Main+App」 import ChatList from
'./pages/ChatList/ChatList'; import Chat from './pages/Chat/Chat'; import Login from './pages/LoginSignup/Login'; import Signup from './pages/LoginSignup/Signup'; import Test from './pages/test'; async function guest() { const [err, res] = await fetch('guest', { os: platform.os.family, browser: platform.name, environment: platform.description, }); if (!err) { action.setGuest(res); } } socket.on('connect', async () => { // await AsyncStorage.setItem('token', ''); const token = await AsyncStorage.getItem('token'); if (token) { const [err, res] = await fetch('loginByToken', Object.assign({ token, }, platform), { toast: false }); if (err) { guest(); } else { action.setUser(res); } } else { guest(); } }); socket.on('disconnect', () => { action.disconnect(); }); socket.on('message', (message) => { // robot10 convertRobot10Message(message); const state = store.getState(); const linkman = state.getIn(['user', 'linkmans']).find(l => l.get('_id') === message.to); let title = ''; if (linkman) { action.addLinkmanMessage(message.to, message); if (linkman.get('type') === 'group') { title = `${message.from.username} 在 ${linkman.get('name')} 對你們說:`; } else { title = `${message.from.username} 對你說:`; } } else { const newLinkman = { _id: getFriendId( state.getIn(['user', '_id']), message.from._id, ), type: 'temporary', createTime: Date.now(), avatar: message.from.avatar, name: message.from.username, messages: [], unread: 1, }; action.addLinkman(newLinkman); title = `${message.from.username} 對你說:`; fetch('getLinkmanHistoryMessages', { linkmanId: newLinkman._id }).then(([err, res]) => { if (!err) { action.addLinkmanMessages(newLinkman._id, res); } }); } // console.log('消息通知', { // title, // image: message.from.avatar, // content: message.type === 'text' ? message.content : `[${message.type}]`, // id: Math.random(), // }); });
----------------------------------------------------------------------------------------------------------
export
default class App extends React.Component { static propTypes = { title: PropTypes.string, }
static async updateVersion() {
if (process.env.NODE_ENV === 'development') { return; } const result = await Updates.fetchUpdateAsync(); if (result.isNew) { Updates.reload(); } else { Alert.alert('提示', '當前版本已是最新了'); } }
render() {
return ( <Provider store={store}> <Root> <Router> <View style={styles.container}> <Scene key="test" component={Test} title="測試頁面" /> <Scene key="chatlist" component={ChatList} title="消息" onRight={App.updateVersion} rightTitle={`v${packageInfo.version}`} initial /> <Scene key="chat" component={Chat} title="聊天" getTitle={this.props.title} /> <Scene key="login" component={Login} title="登陸" backTitle="返回聊天" /> <Scene key="signup" component={Signup} title="註冊" backTitle="返回聊天" /> </View> </Router> </Root> </Provider> ); } } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', }, });

 

RN路由詳見:[RN] 04 - "react-native-router-flux"

 

 

3、首頁之會話通道

聊天通道列表,也就是ChatList。

在此,好友和聊天通道是混淆在一塊兒的,能聊則默認是好友。

 

回話通道列表:

import React, { Component } from 'react';
import { ScrollView } from 'react-native'; import { Container } from 'native-base';
import { connect }          from 'react-redux';
import ImmutablePropTypes   from 'react-immutable-proptypes';

import Linkman from './Linkman';

class ChatList extends Component {

static propTypes
= { linkmans: ImmutablePropTypes.list, }
-------------------------------------------------------------------------------------- static renderLinkman(linkman) { const linkmanId
= linkman.get('_id'); const unread = linkman.get('unread'); const lastMessage = linkman.getIn(['messages', linkman.get('messages').size - 1]); let time = new Date(linkman.get('createTime')); let preview = '暫無消息'; if (lastMessage) { time = new Date(lastMessage.get('createTime')); preview = `${lastMessage.get('content')}`; if (linkman.get('type') === 'group') { preview = `${lastMessage.getIn(['from', 'username'])}: ${preview}`; } } return ( <Linkman key={linkmanId} id={linkmanId}  // 以後路由跳轉時有用 name={linkman.get('name')} avatar={linkman.get('avatar')} preview={preview} time={time} unread={unread} /> ); }
render() { const { linkmans }
= this.props; return ( <Container> <ScrollView> { linkmans && linkmans.map(linkman => ( ChatList.renderLinkman(linkman) )) } </ScrollView> </Container> ); } } export default connect(state => ({ linkmans: state.getIn(['user', 'linkmans']), }))(ChatList);

單個回話Linkman新增了一個屬性:id,爲了點擊事件後的路由跳轉。

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Text, StyleSheet, View, TouchableOpacity } from 'react-native';

import autobind from 'autobind-decorator';
import { Actions } from 'react-native-router-flux';

import Avatar from '../../components/Avatar';
import Time   from '../../../utils/time';
import action from '../../state/action';


export default class Linkman extends Component {
    static propTypes = {
        id:      PropTypes.string.isRequired,
        name:    PropTypes.string.isRequired,
        avatar:  PropTypes.string.isRequired,
        preview: PropTypes.string,
        time:    PropTypes.object,
        unread:  PropTypes.number,
    }
    formatTime() {
        const { time: messageTime } = this.props;
        const nowTime = new Date();
        if (Time.isToday(nowTime, messageTime)) {
            return Time.getHourMinute(messageTime);
        }
        if (Time.isYesterday(nowTime, messageTime)) {
            return '昨天';
        }
        return Time.getMonthDate(messageTime);
    }
@autobind handlePress() { const { name, id }
= this.props; action.setFocus(id); Actions.chat({ title: name });  // --> 與網頁不一樣,應該是路由跳轉到新頁面 }
render() { const { name, avatar, preview, unread }
= this.props; return ( <TouchableOpacity onPress={this.handlePress}> <View style={styles.container}> <Avatar src={avatar} size={50} /> <View style={styles.content}> <View style={styles.nickTime}> <Text style={styles.nick}>{name}</Text> <Text style={styles.time}>{this.formatTime()}</Text> </View> <View style={styles.previewUnread}> <Text style={styles.preview} numberOfLines={1}>{preview}</Text> { unread > 0 ? <View style={styles.unread}> <Text style={styles.unreadText}>{unread}</Text> </View> : null } </View> </View> </View> </TouchableOpacity> ); } } const styles = StyleSheet.create({ ... });

 

 

4、次頁之會話內容 

  • 基本框架

三大部分:輸入框,消息列表,消息顯示。

平心而論,Feature差別挺大,仍是不參考WEB,從新實現RN爲好。

用到的State基本一致。

class Chat extends Component {
    render() {
        return (
            <KeyboardAvoidingView style={styles.container} behavior="padding" keyboardVerticalOffset={isiOS ? 64 : 80}>
                <Container style={styles.container}>
                    <MessageList />
                    <Input />
                </Container>
            </KeyboardAvoidingView>
        );
    }
}

 

  • 消息列表
render() {
        const { messages } = this.props;
        const { imageViewerDialog, imageViewerIndex } = this.state;
        const closeImageViewer = openClose.close.bind(this, 'imageViewerDialog');
        return (
            <ScrollView
                style={styles.container}
                ref  ={i => this.scrollView = i}
                refreshControl={
                    <RefreshControl
                        refreshing={this.state.refreshing}
                        onRefresh ={this.handleRefresh}
                        title     ="獲取歷史消息"
                        titleColor="#444"
                    />
                }
                onContentSizeChange={this.handleContentSizeChange}
            >
                {
                    messages.map((message, index) => (
                        this.renderMessage(message, index === messages.size - 1)
                    ))
                }
                ...
...
</ScrollView> ); }

map逐條渲染Message。

    renderMessage(message, shouldScroll) {
        const { self } = this.props;
        const props = {
            key:         message.get('_id'),
            avatar:      message.getIn(['from', 'avatar']),
            nickname:    message.getIn(['from', 'username']),
            time:        new Date(message.get('createTime')),
            type:        message.get('type'),
            content:     message.get('content'),
            isSelf:      self === message.getIn(['from', '_id']),
            tag:         message.getIn(['from', 'tag']),
            shouldScroll,
            scrollToEnd: this.scrollToEnd,
        };
        if (props.type === 'image') {
            props.loading = message.get('loading');
            props.percent = message.get('percent');
            props.openImageViewer = this.openImageViewer;
        }
        return (
            <Message {...props} />
        );
    }

 

  • 對話框
 renderContent() {
        const { type } = this.props;
        switch (type) {
        case 'text': {
            return this.renderText();
        }
        case 'image': {
            return this.renderImage();
        }
        default:
            return (
                <Text style={styles.notSupport}>不支持的消息類型, 請在Web端查看</Text>
            );
        }
    }
render() { const { avatar, nickname, isSelf }
= this.props; return ( <View style={[styles.container, isSelf ? styles.containerSelf : styles.empty]}> <Avatar src={avatar} size={44} /> <View style={[styles.info, isSelf ? styles.infoSelf : styles.empty]}> <View style={styles.nickTime}> <Text style={styles.nick}>{nickname}</Text> <Text style={styles.time}>{this.formatTime()}</Text> </View> <View style={styles.content}> {this.renderContent()} </View> </View> </View> ); }

 

  • 輸入框

主要是兩個動做:

 (1) 本地渲染一條消息的action;

 (2) 異步動做發送一條消息,以下;

    @autobind
    async sendMessage(localId, type, content) {
        const { focus } = this.props;
        const [err, res] = await fetch('sendMessage', {
            to: focus,
            type,
            content,
        });
        if (!err) {
            res.loading = false;
            action.updateSelfMessage(focus, localId, res);
        }
    }

 

 

5、Redux 的代碼複用

最後就是state文件夾下的store, reducer, action。

代碼基本能夠徹底拷貝過來用,畢竟邏輯是一致的,只是view不一樣而已。

這也是React-Redux美妙的地方!

 

Redux 基本上就是如此,繼續深刻則須要大量實踐來體會內涵。 

相關文章
相關標籤/搜索