前言

最近一段时间都在折腾ReactNative,经过两年发展,RN已经相对成熟,应用的性能和流畅度并不比原生逊色多少,并且一次开发不仅仅可以作用于Android,iOS平台,还可以兼容到MacOS和Windows平台,还是很值得一学的。且js本身就是一门动态语言,再加上JSX语法允许把逻辑和UI组合在一起,再配合高效的操作符和函数式编程,开发体验也很不错。但是在debug方面还是不及原生方便,错误提示和定位不够明确,一些非逻辑性的问题(比如环境,配置问题等)也很让人头疼。这篇文章主要是对最近一段时间做ReactNative和Redux开发的一些总结,主要包括三方面内容:

  • ReactNative、Redux的一些踩坑和心得
  • 基于protobuf.js和socket封装通信层
  • js和native互相调用的总结

ReactNative、Redux踩坑和心得

0.增加新依赖或去除旧依赖后需要重启Packager,否则会报could not resolve all dependencies for configuration ':app:debugAPKCopy',入门踩的第一个坑 =.=
使用npm start -- --reset-cache指令重启 或者ctrl/cmd + C关进程

1.当自己封装react-native的Android原生库,且该库还依赖了aar文件时,需要在当前react-native工程中额外指明aar所在路径。比如aar文件在react-native-ucloudsdk这个封装库的android/libs目录下,则在ReactNativeProject/android/build.gradle中添加以下标注的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
allprojects {
    repositories {
        mavenLocal()
        jcenter()
        maven {
            url "$rootDir/../node_modules/react-native/android"
        }
        //添加如下部分,指明依赖路径,否则找不到aar
        flatDir{
            dirs "$rootDir/../node_modules/react-native-ucloudsdk/android/libs"
        }
    }
}

2.当0.45.x版本的react-native编译iOS时会报错,需要回退到0.44.x版本才可以编译通过
报错内容":CFBundleIdentifier", Does Not Exist
package.json中需要替换的依赖:

1
2
"react": "^16.0.0-alpha.12",
"react-native": "^0.45.1",

替换为

1
2
"react": "^16.0.0-alpha.6",
"react-native": "^0.44.3",

下载旧版依赖,清理缓存后再次执行react-native run-ios即可编译通过

3.react-native中存在负责计算的JS线程和负责绘制的UI线程,大量的console.log也会拖慢JS线程造成响应迟钝,打包release版时注意去除不必要的console.log

4.基于react-navigation的换肤策略。虽然redux作为全局状态管理器可以作用于各组件,但大部分应用的title使用的应该都是路由框架react-navigation自带的组件,我们没办法直接接触到这个组件。调查后发现StackNavigator具备一个可以传递到所有页面的属性screenProps,借助这个属性可以达成我们改变所有页面title样式的目的,直接将订阅了store的属性传递给RootNavigatorscreenProps即可。之后在每个页面的navigationOptions中都能收到传递来的screenProps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class App extends Component {
render() {
return (
<RootNavigator screenProps={this.props.screenProps}/>
);
}
}
const mapStateToProps = (state) => {
return {
screenProps: state.themes.currentTheme,
}
};

5.redux的mapStateToProps函数中收到的state是经过reduce类名包装的

1
2
3
4
5
6
7
const mapStateToProps = (state) => {
    return {
        currentSong: state.songs.currentSong,
       playMode: state.songs.playMode,
       isPlaying: state.songs.isPlaying
    }
};

我曾写成如下形式,状态更新后却没有re-render,debug了很久:

1
2
3
4
5
6
7
const mapStateToProps = (state) => {
    return {
        currentSong: state.currentSong,
       playMode: state.playMode,
       isPlaying: state.isPlaying
    }
};

6.注意组件的拆分粒度,如果一个组件可以被拆分成多个组件则尽量拆分,这样redux刷新props时只刷新需要的组件即可,否则重绘一个未拆分的大组件可能会造成不必要的开销。比如MoeFM中的播放界面组件PlayerUI就被拆分成了数个小组件,每个按键都是一个独立的组件。在拆分之前每当props刷新时界面都会闪烁,这是因为每个小组件需要监听的props都注册在大组件中,刷新频繁,且每次刷新中还包含了一些不必要的成分。

7.ReactNative中的动画是没有infinite(无限执行)属性的,想让动画持续执行只能在动画执行结束后设回初值再重新执行,start方法中的回调会在动画结束后调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
runAnimation(value) {
this.isRunning = true;
this.state.rotateAnim.setValue(value);
Animated.timing(
this.state.rotateAnim,
{
toValue: 360,
easing: Easing.linear,
duration: 20000 * (360 - value) / 360,
}
).start((event) => {
if (event.finished) {
this.runAnimation(0);
}
});
}

8.当自己封装原生控件时如果出现has no proptype for native prop accessibilitylabel of native type String这种错误,是由于缺少了一些基本属性,添加ViewPropTypes即可

1
2
3
4
5
6
import { ViewPropTypes } from "react-native";
propTypes: {
...ViewPropTypes,
myProp: PropTypes.string
}

9.redux中的reducer是纯函数(即每次输入相同的内容必然可以得到相同的结果),为了保证它的纯净性只能用它来更新state,不应该做额外的逻辑处理。如果在state发生变更的前后需要插入额外的逻辑,则应该通过middleware(中间件)来完成,在中间件中能够获得store,action,action前的state,action后的state,足以处理复杂的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
const playerMiddleware = store => next => action => {
const thisState = store.getState();
const result = next(action);
const newState = store.getState();
const actionType = String(action.type);
if (newState === thisState) {
return result;
}
switch (actionType) {
case ADD_SONGS:
break;
}
}

ReactNative上搭建基于Protobuf.js+Socket的通信模块

之前在Android端上实现过基于protobuf协议的socket通信,最近在ReactNative上也做了这么一次实践。protobuf在js上的实现使用的是protobuf.js库,socket使用的是react-native-tcp这么一个封装了Android与iOS原生Socket的库,先说下几个坑点:

  • ReactNative只能加载.js和.json,不能直接加载.proto文件,所以在使用protobuf.js时需要先用该库提供的Command Line工具,把.proto文件转换成.json格式,然后再加载并读取json。
  • ReactNative自带的Websocket模块和主流开源库socket.io-client都只能以websocket的方式和服务端连接。如果服务端不支持websocket协议,那么还是需要用react-native-tcp来做通信,因为这个库导出了Android和iOS的原生Socket。
  • react-native-tcp直接安装下来可能会报错,可以参考issue-42的方式解决。该库没有提供API文档,因为它的API直接参考了node.js的net模块,参考net模块文档即可

然后开始搭建通信模块,首先是编写.proto文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// BaseMsg.proto
syntax = "proto3";
enum MSG_TYPE{
MSG_TYPE_START = 0;
eSignal = 0x00000001;
ePChannelMediaMsg = 0x00001000;
eRequestLoginPMedia = 0x00001001;
eReturnLoginPMedia = 0x00001002;
}
// PChannelMediaMsg.proto
syntax = "proto3";
package PChannelMediaMsg;
import "BaseMsg.proto";
message RequestLoginPMedia {
MSG_TYPE msg = 1;// [default = eRequestLoginPMedia];
uint32 userid = 2;
}
message ReturnLoginPMedia{
MSG_TYPE msg = 1;// [default = eReturnLoginPMedia];
uint32 userid = 2;
}

npm install -g protobufjs 全局安装protobufjs,执行转化命令pbjs -t json BaseMsg.proto PChannelMediaMsg.proto > bundle.json 得到bundle.json文件。

然后专门编写一个模块解析protobuf文件,并把解析结果都记录到Map中,方便之后使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import MsgType from './MsgType'
let Proto = require('./bundle.json');
let ProtobufRoot = require('protobufjs').Root;
let root = ProtobufRoot.fromJSON(Proto);
const MessageMap = new Map();
let BaseMsg = root.lookupType("BaseMsg");
MessageMap.set('Base', BaseMsg);
let RequestLoginPMedia = root.lookupType("PChannelMediaMsg.RequestLoginPMedia");
MessageMap.set(MsgType.eRequestLoginPMedia, RequestLoginPMedia);
let ReturnLoginPMedia = root.lookupType("PChannelMediaMsg.ReturnLoginPMedia");
MessageMap.set(MsgType.eReturnLoginPMedia, ReturnLoginPMedia);
export default MessageMap;

最后编写Socket模块,主要包括连接、断开、发送、接收、维持心跳包、附加与解析头部字段这些功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import MessageMap from './Protobuf'
let net = require('react-native-tcp');
let client = null;
let callback = null;
let beatInterval = null;
let recvData = new Uint8Array();
let isConnect = false;
const options = {
host: '192.168.1.1',
port: '60000',
};
const beat = new Uint8Array([5,3,3,0,1,2]);
const HEART_BEAT_RATE = 1000 * 60;
const RECONNECT_RATE = 1000 * 2;
//从MessageMap中取出对应的proto类型,完成 Uint8Array -> obj
function parseRecvData(data) {
//protobuf 基础反序列化
let BaseMsg = MessageMap.get('Base');
let type = BaseMsg.toObject(BaseMsg.decode(data)).msg;
//protobuf 二次反序列化
let Message = MessageMap.get(type);
if (Message === undefined)
return;
let obj = Message.toObject(Message.decode(data));
if (callback !== null) {
callback(type, obj);
}
}
const connect = () => {
isConnect = true;
client = net.createConnection(options, () => {
if (callback !== null) {
callback('connect', null);
}
beatInterval = setInterval(() => {
client.write(new Buffer(beat));
}, HEART_BEAT_RATE)
});
client.on('error', function(error) {
clearInterval(beatInterval);
if (isConnect) {
//尝试重连
setTimeout(() => connect(), RECONNECT_RATE);
}
});
client.on('close', function() {
clearInterval(beatInterval);
if (isConnect) {
//尝试重连
setTimeout(() => connect(), RECONNECT_RATE);
}
});
//接收二进制字节流
//取前4个字节作为头部,解析出消息的实际长度
client.on('data', function(data) {
let lastData = recvData;
recvData = new Uint8Array(lastData.length + data.length);
recvData.set(lastData);
recvData.set(data, lastData.length);
while (recvData.length > 4) {
let head = new Uint8Array(4);
head[0] = recvData[0];
head[1] = recvData[1];
head[2] = 0;
head[3] = 0;
let length = new DataView(head.reverse().buffer).getInt32(0, false);
if (recvData.length - 4 < length) {
break;
} else {
for(let i = 0;i < 4; i++) {
recvData = recvData.slice(1);
}
}
let msg = [];
for (let i = 0; i < length ; i++) {
msg[i] = recvData[0];
recvData = recvData.slice(1);
}
parseRecvData(msg);
}
});
};
const disconnect = () => {
isConnect = false;
client.destroy();
};
//发送二进制字节流
//从MessageMap中取出对应的proto类型,完成 obj -> Uint8Array
//发送前额外插入4字节作为头部,其中低位的两个字节记录消息长度
const send = (obj) => {
let Message = MessageMap.get(obj.msg);
let data = Message.encode(Message.fromObject(obj)).finish();
let head = new Uint8Array(4);
new DataView(head.buffer).setInt32(0, data.length);
head = head.reverse();
let msg = new Uint8Array(head.length + data.length);
msg.set(head);
msg.set(data, head.length);
client.write(new Buffer(msg.buffer)); // send a message
};
const setCallback = (on) => {
callback = on;
};
export {
connect,
disconnect,
send,
setCallback
};

ReactNative中原生层与js层互发消息的几种姿势

ReactNative允许我们编写Native层的代码,来应对一些高性能、多线程的场景或是ReactNative尚未实现的一些功能。可以通过继承SimpleViewManager<T>类来编写原生UI组件、继承ReactContextBaseJavaModule类来编写原生功能模块。具体写法可以看文档,这里主要梳理一下当写好Native层后,如何实现以下消息的传递:

  • JS -> 原生模块
  • 原生模块 -> JS
  • JS -> 原生UI组件
  • 原生UI组件 -> JS

JS -> 原生模块

原生模块在和JS层的通信上要比原生UI组件方便不少,直接用ReactNative提供的@ReactMethod就可以导出供JS层调用的方法了

1
2
3
4
5
//Native
@ReactMethod
public void show(String message, int duration) {
Toast.makeText(getReactApplicationContext(), message, duration).show();
}
1
2
3
//JS
import ToastAndroid from './ToastAndroid';
ToastAndroid.show('Awesome', ToastAndroid.SHORT);

原生模块 -> JS

通过Callback类的invoke方法来实现原生模块向JS层发消息。JS层在Callback参数对应的位置传入函数,当invoke方法触发后会触发对应函数,并提供参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Native
@ReactMethod
public void measureLayout(
int tag,
int ancestorTag,
Callback errorCallback,
Callback successCallback) {
try {
measureLayout(tag, ancestorTag, mMeasureBuffer);
float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
successCallback.invoke(relativeX, relativeY, width, height);
} catch (IllegalViewOperationException e) {
errorCallback.invoke(e.getMessage());
}
}
1
2
3
4
5
6
7
8
9
10
11
//JS
UIManager.measureLayout(
100,
100,
(msg) => {
console.log(msg);
},
(x, y, width, height) => {
console.log(x + ':' + y + ':' + width + ':' + height);
}
);

JS -> 原生UI组件

原生UI组件在和JS层的通信上要相对复杂一些,需要重写SimpleViewManager类的getCommandsMapreceiveCommand方法。getCommandsMap定义了方法名和指令序号的映射,receiveCommand定义了收到每种指令序号时需要执行的原生操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//Native
private static final int CONNECT = 0;
private static final int DESTROY = 1;
private static final int SEND_JSON_TO_JS = 2;
@Override
public Map<String,Integer> getCommandsMap() {
Log.d("React"," View manager getCommandsMap:");
return MapBuilder.of(
"connect",
CONNECT,
"destroy",
DESTROY,
"sendJsonToJS",
SEND_JSON_TO_JS);
}
@Override
public void receiveCommand(WebBridge root, int commandId, @Nullable ReadableArray args) {
Assertions.assertNotNull(root);
Assertions.assertNotNull(args);
switch (commandId) {
case CONNECT: {
if (args != null) {
root.loadUrl(args.getString(0));
}
break;
}
case DESTROY: {
if (root != null) {
root.setWebViewClient(null);
root.removeAllViews();
root.destroy();
}
break;
}
case SEND_JSON_TO_JS: {
if (root != null) {
String call = null;
if (args != null) {
call = "javascript:controller.getJsonFrom(" + args.getString(0) + ", " + args.getString(1) + ")";
}
root.loadUrl(call);
}
break;
}
default:
throw new IllegalArgumentException(String.format(
"Unsupported command %d received by %s.",
commandId,
getClass().getSimpleName()));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//JS
import {Component, PropTypes } from 'react';
import { requireNativeComponent, ViewPropTypes, UIManager, findNodeHandle } from 'react-native';
import * as React from "react";
const RTCWebBridge = requireNativeComponent('RTCWebBridge', WebBridge, {
nativeOnly: { onChange: true }
});
export default class WebBridge extends Component {
constructor() {
super();
this.onChange = this.onChange.bind(this);
}
componentDidMount() {
this.mapViewHandle = findNodeHandle(this.mapViewRef);
}
onChange(event) {
if (this.props.onJsToPhone) {
this.props.onJsToPhone({
cmd: event.nativeEvent.cmd,
json: event.nativeEvent.json,
})
}
}
render() {
return (
<RTCWebBridge {...this.props} onChange={this.onChange} ref={(mv) => this.mapViewRef = mv} />
);
}
connect(url) {
UIManager.dispatchViewManagerCommand(
this.mapViewHandle,
UIManager.RTCWebBridge.Commands.connect,
[url],
);
}
destroy() {
UIManager.dispatchViewManagerCommand(
this.mapViewHandle,
UIManager.RTCWebBridge.Commands.destroy,
[],
);
}
sendJsonToJS(cmd, json) {
UIManager.dispatchViewManagerCommand(
this.mapViewHandle,
UIManager.RTCWebBridge.Commands.sendJsonToJS,
[cmd, json],
);
}
}
WebBridge.propTypes = {
...ViewPropTypes,
};

在JS层为了方便使用,将之前写的原生UI组件RTCWebBridge又额外包装成了一个新组件WebBridge,对外提供了connectdestroy等一系列方法。

可以看到当JS层想调用原生UI组件的方法时,实际是依靠UIManager.dispatchViewManagerCommand方法来完成的。这个方法需要传入3个参数,第一个参数是原生UI组件的引用,第二个参数是我们之前在Native层的getCommandsMap中定义的方法名,第三个参数是本次方法调用中需要用到的参数。当发生一次dispatchViewManagerCommand操作时,被指定的原生UI组件会先从CommandsMap取指令序号,然后把指令序号和参数一起传到receiveCommand里完成本次方法调用。由此实现了JS -> 原生UI组件的消息传递。

原生UI组件 -> JS

原生UI组件向JS传递消息时,需要把参数封装到Event里,然后将该Event作为一个topChange事件发射出去。

1
2
3
4
5
6
7
8
9
//Native
WritableMap event = Arguments.createMap();
event.putString("message", "MyMessage");
ReactContext reactContext = (ReactContext)getContext();
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
getId(),
"topChange",
event);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MyCustomView extends React.Component {
constructor() {
this._onChange = this._onChange.bind(this);
}
_onChange(event: Event) {
if (!this.props.onChangeMessage) {
return;
}
this.props.onChangeMessage(event.nativeEvent.message);
}
render() {
return <RCTMyCustomView {...this.props} onChange={this._onChange} />;
}
}
MyCustomView.propTypes = {
/**
* Callback that is called continuously when the user is dragging the map.
*/
onChangeMessage: React.PropTypes.func,
...
};
var RCTMyCustomView = requireNativeComponent(`RCTMyCustomView`, MyCustomView, {
nativeOnly: {onChange: true}
});

JS层会在导出的原生UI组件的onChange方法里收到topChange事件。至于为什么topChange会被映射成onChange这个是官方事先定义好的,可以去看源码中的UIManagerModuleConstants.java文件,在这里还能看到更多事件映射,不仅仅局限于topChangeevent.nativeEvent就是我们之前在Native层封装的Event,从中可以取到本次消息传递包含的参数,由此JS收到了Native发来的数据。

最后

最近基于ReactNative和Redux写了一个支持Android与iOS双平台的音乐播放器应用MoeFM,感兴趣的话可以去看看,欢迎提issue (๑´ㅂ`๑)

声明:本站所有文章均为原创或翻译,遵循署名-非商业性使用-禁止演绎 4.0 国际许可协议,如需转载请确保您对该协议有足够了解,并附上作者名(Est)及原贴地址