本篇屬於私人筆記。html
├── assets
│ ├── audios
│ ├── fonts
│ └── images
├── 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.html【app】--> main.js (膠水角色) --> App.js【UI組件】webpack
main.js
ReactDom.render( <Provider store={store}> <App /> </Provider>, document.getElementById('app'), );
異步通訊涉及到的socket.io部分。【待定】ios
沒有第二個參數: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 中獲取,用於顯示;框架
├── modules
│ └── main
│ ├── Main.jsx
│ ├── Main.less
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> ); }
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),容許向一個現有的對象添加新的功能,同時又不改變其結構。這種類型的設計模式屬於結構型模式,它是做爲現有的類的一個包裝。
├── modules
│ └── main
│ ├── chatPanel
│ └── sidebar
│ ├── AppDownload.jsx
│ ├── OnlineStatus.jsx
│ ├── Sidebar.jsx
│ ├── Sidebar.less
│ ├── SingleCheckButton.jsx
│ └── SingleCheckGroup.jsx
siderBar是個獨立的子模塊,等價於setting page。
[sideBar/Sidebar.jsx]
[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
import { createStore } from 'redux'; import reducer from './reducer'; const store = createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), );
export default store;
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; } ...
----> 這裏有值得玩味的地方,以前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的動做信號。
除了 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', }, });
聊天通道列表,也就是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); } }
最後就是state文件夾下的store, reducer, action。
代碼基本能夠徹底拷貝過來用,畢竟邏輯是一致的,只是view不一樣而已。
這也是React-Redux美妙的地方!
Redux 基本上就是如此,繼續深刻則須要大量實踐來體會內涵。