本篇属于私人笔记。

client 引导部分


一、assets: 音频,图片,字体

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

二、main"胶水"函数

├── App.jsx
├── App.less
├── main.js
└── templates
└── index.html
  • 引导入口

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

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.html【app】--> main.js (胶水角色) --> App.js【UI组件】

main.js

ReactDom.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app'),
);
  • 初始化功能

异步通信涉及到的socket.io部分。【待定】

client's Redux 部分


一、谁是第一个 container

  • connect 函数

没有第二个参数:mapDispatchToProps

如下可见,状态有很多属性。

[state/reducer.js]

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中获取状态,用于显示;

(2). componentDidMount 中获取,用于显示;

二、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),允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

三、嵌套下的"第二个 container"

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

siderBar是个独立的子模块,等价于setting page。

[sideBar/Sidebar.jsx]

四、另外两个 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还有很多。

五、动作信号

├── 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 移植


一、代码结构

除了 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

二、引导部分

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"

三、首页之会话通道

聊天通道列表,也就是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({
...
});

四、次页之会话内容

  • 基本框架

三大部分:输入框,消息列表,消息显示。

平心而论,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);
}
}

五、Redux 的代码复用

最后就是state文件夹下的store, reducer, action。

代码基本可以完全拷贝过来用,毕竟逻辑是一致的,只是view不同而已。

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

Redux 基本上就是如此,继续深入则需要大量实践来体会内涵。

[React] 15 - Redux: practice IM的更多相关文章

  1. 基于 React.js + Redux + Bootstrap 的 Ruby China 示例 (转)

    一直学 REACT + METEOR 但路由部分有点问题,参考一下:基于 React.js + Redux + Bootstrap 的 Ruby China 示例 http://react-china ...

  2. 从零开始配置TypeScript + React + React-Router + Redux + Webpack开发环境

    转载请注明出处! 说在前面的话: 1.为什么不使用现成的脚手架?脚手架配置的东西太多太重了,一股脑全塞给你,我只想先用一些我能懂的库和插件,然后慢慢的添加其他的.而且自己从零开始配置也能学到更多的东西 ...

  3. 教你如何在React及Redux项目中进行服务端渲染

    服务端渲染(SSR: Server Side Rendering)在React项目中有着广泛的应用场景 基于React虚拟DOM的特性,在浏览器端和服务端我们可以实现同构(可以使用同一份代码来实现多端 ...

  4. immutable.js 在React、Redux中的实践以及常用API简介

    immutable.js 在React.Redux中的实践以及常用API简介 学习下 这个immutable Data 是什么鬼,有什么优点,好处等等 mark :  https://yq.aliyu ...

  5. 实例讲解react+react-router+redux

    前言 总括: 本文采用react+redux+react-router+less+es6+webpack,以实现一个简易备忘录(todolist)为例尽可能全面的讲述使用react全家桶实现一个完整应 ...

  6. 升级react 15.4,常见的错误及解决方案

    最近项目由react0.14.X升级到react 15版本,因为react15还是做了一些相对大一点的更新的(详情可以参考一下我的另一篇文章关于react15的一点总结),相对:来说react升级之后 ...

  7. 基于react+react-router+redux+socket.io+koa开发一个聊天室

    最近练手开发了一个项目,是一个聊天室应用.项目虽不大,但是使用到了react, react-router, redux, socket.io,后端开发使用了koa,算是一个比较综合性的案例,很多概念和 ...

  8. 最新的chart 聊天功能( webpack2 + react + router + redux + scss + nodejs + express + mysql + es6/7)

    请表明转载链接: 我是一个喜欢捣腾的人,没事总喜欢学点新东西,可能现在用不到,但是不保证下一刻用不到. 我一直从事的是依赖angular.js 的web开发,但是我怎么能一直用它呢?看看最近火的一塌糊 ...

  9. 【前端】react and redux教程学习实践,浅显易懂的实践学习方法。

    前言 前几天,我在博文[前端]一步一步使用webpack+react+scss脚手架重构项目 中搭建了一个react开发环境.然而在实际的开发过程中,或者是在对源码的理解中,感受到react中用的最多 ...

随机推荐

  1. Oracle INTERVAL

    转自:http://www.cnblogs.com/ungshow/archive/2009/04/11/1433747.html INTERVAL DAY TO SECOND数据类型 Oracle语 ...

  2. spring源码分析系列 (8) FactoryBean工厂类机制

    更多文章点击--spring源码分析系列 1.FactoryBean设计目的以及使用 2.FactoryBean工厂类机制运行机制分析 1.FactoryBean设计目的以及使用 FactoryBea ...

  3. Maven入门指南⑥:将项目发布到私服

    1 . 修改私服中仓库的部署策略 Release版本的项目应该发布到Releases仓库中,对应的,Snapshot版本应该发布到Snapshots仓库中.Maven根据pom.xml文件中版本号&l ...

  4. history.pushState无刷新改变url

    通过history.pushState无刷新改变url 背景 在浏览器中改变地址栏url,将会触发页面资源的重新加载,这使得我们可以在不同的页面间进行跳转,得以浏览不同的内容.但随着单页应用的增多,越 ...

  5. mysql报错:1130 -host 'localhost' is not allowed to connect to this mysql server

    错误提示:1130 -host 'localhost' is not allowed to connect to this mysql server 原因:手贱把mysql数据库系统中mysql数据库 ...

  6. zookeeper leader选举机制

    最近看了下zookeeper的源码,先整理下leader选举机制 先看几个关键数据结构和函数 服务可能处于的状态,从名字应该很好理解 public enum ServerState { LOOKING ...

  7. facebook's HipHop for PHP: Move Fast

    One of the key values at Facebook is to move fast. For the past six years, we have been able to acco ...

  8. 发布库到仓库 maven jcenter JitPack MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  9. Java代码里利用Fiddler抓包调试设置

    Fiddler启动时已经将自己注册为系统的默认代理服务器,应用程序在访问网络时会去获取系统的默认代理,如果需要捕获java访问网络时的数据,只需要在启动java程序时设置代理服务器为Fiddler即可 ...

  10. 从NSTimer的失效性谈起(二):关于GCD Timer和libdispatch

    一.GCD Timer的创建和安放 尽管GCD Timer并不依赖于NSRunLoop,可是有没有可能在某种情况下,GCD Timer也失效了?就好比一開始我们也不知道NSTimer相应着一个runl ...