首页 技术 正文
技术 2022年11月10日
0 收藏 833 点赞 3,230 浏览 13332 个字

本篇属于私人笔记。

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.jsReactDom.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app'),
);
  • 初始化功能

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

client’s Redux 部分


一、谁是第一个 container

  • connect 函数

没有第二个参数:mapDispatchToProps

[React] 15 – Redux: practice IM

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

[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]

[React] 15 – Redux: practice IM

四、另外两个 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: [],
}),
] },
});
}

—-> 大括号自动处理动作信号。

[React] 15 – Redux: practice IM

下图中的大括号{…}就有了默认执行dispatch的意思,即执行上图return的动作信号。

[React] 15 – Redux: practice IM

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。

在此,好友和聊天通道是混淆在一起的,能聊则默认是好友。

[React] 15 – Redux: practice IM

回话通道列表:

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 基本上就是如此,继续深入则需要大量实践来体会内涵。

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:9,135
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,603
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,446
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,220
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,855
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,941