1. 屏幕适配
RN布局使用的单位是dp,而开发人员从设计稿最方便获取的是px,所以需要一个工具类把px转成dp,下面以宽度为375px的设计稿为例:
const deviceWidthDp = Dimensions.get('window').width;
const uiWidthPx = 375;
export default function pxtodp(uiElementPx) {
return Platform.OS === 'ios' ? uiElementPx * deviceWidthDp / uiWidthPx : Math.floor(uiElementPx * deviceWidthDp / uiWidthPx)
}
在bug修复阶段,发现一个常见的bug,多组件传值时,出现了多次的p2dp嵌套,导致了值被转换多次,不符预期,所以写组件的时,应该规定好是最底层使用p2dp,还是传入的参数使用p2dp。
2. 样式管理
- RN的样式可以是数组,类似css中定义多个class;
style={[styles.A,styles.B]}
- RN的样式没有继承嵌套这类的功能,为了方便、高效使用样式,我们使用了一个样式的工具类,写入常用的样式、样式组合,便于页面调用。但每个页面调用都要引入工具类太麻烦,考虑注册到全局变量,这时候发现了一个问题,RN的
global
(全局变量)只能作用于Component
,在StyleSheet
无法识别,难道是根据某种上下文关系存在的?最后找到一个解决方案是我们写一个函数,在函数内是就能访问全局变量,然后把StyleSheet
在函数中return出来,代码片段像这样:
const styles = () => {
const {
paddingLarge, paddingSmall, paddingMedium,
fontSizeMedium, fontSizeSmall, fontWeightLight
} = theme
return StyleSheet.create({
wrapper: {
height: px2dp(106),
marginHorizontal: paddingLarge,
3. 平台差异
使用react-native的Platform
库来控制android和ios的差异
Platform.OS === 'ios' ? doios : doandroid
新版api还可以这么写:
const instructions = Platform.select({
ios: 'Press Cmd+R to reload,\n' +
'Cmd+D or shake for dev menu',
android: 'Double tap R on your keyboard to reload,\n' +
'Shake or press menu button for dev menu',
});
另外如何需要根据平台引入不同的组件,例如:
BigButton.ios.js
BigButton.android.js
你可以直接:
const BigButton = require('./BigButton');
React Native 会自动识别。
4. 点击
RN上除了Text
组件(自带onPress
方法),其他组件默认是不支持点击事件。所以 RN 中提供了几个直接处理响应事件的组件,基本上能够满大部分的点击处理需求TouchableHighlight
, TouchableNativeFeedback
, TouchableOpacity
和 TouchableWithoutFeedback
。因为这几个组件的功能和使用方法基本类似,只是 Touch 的反馈效果不一样,所以根据需求选用合适的方法使用即可。
另外,如果在Touchable中onPress执行了一个setState的操作,这个操作需要大量计算工作并且导致了掉帧,这时候可以将操作封装到requestAnimationFrame
中:
handleOnPress() {
// 谨记在使用requestAnimationFrame、setTimeout以及setInterval时
// 要使用TimerMixin(其作用是在组件unmount时,清除所有定时器)
this.requestAnimationFrame(() => {
this.doExpensiveAction();
});
}
5. 手势识别
RN 提供了内置的手势识别库PanResponder
,我们只需要创建一个实例,然后搭载在任意的区域,就能监听到这块区域的手势变化,代码片段如下:
componentWillMount: function() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderMove: this._handlePanResponderMove,
onPanResponderRelease: this._handlePanResponderEnd,
onPanResponderTerminate: this._handlePanResponderEnd,
});
}
但这个手势库PanResponder
有个bug,会blockTouchableWithoutFeedback/Highlight
等的点击操作,解决方案是:
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
return Math.abs(gestureState.dx) > 5;
},
具体可以看这个issue。
6. 文字行数控制
RN提供了numberOfLines
方法实现行数控制,以及溢出部分的处理,同css的text-overflow
。
7. 样式表
RN的布局主要使用flex
,而且是阉割版的flex
,样式表大致只有如下属性:
"alignItems",
"alignSelf",
"backfaceVisibility",
"backgroundColor",
"borderBottomColor",
"borderBottomLeftRadius",
"borderBottomRightRadius",
"borderBottomWidth",
"borderColor",
"borderLeftColor",
"borderLeftWidth",
"borderRadius",
"borderRightColor",
"borderRightWidth",
"borderStyle",
"borderTopColor",
"borderTopLeftRadius",
"borderTopRightRadius",
"borderTopWidth",
"borderWidth",
"bottom",
"color",
"flex",
"flexDirection",
"flexWrap",
"fontFamily",
"fontSize",
"fontStyle",
"fontWeight",
"height",
"justifyContent",
"left",
"letterSpacing",
"lineHeight",
"margin",
"marginBottom",
"marginHorizontal",
"marginLeft",
"marginRight",
"marginTop",
"marginVertical",
"opacity",
"overflow",
"padding",
"paddingBottom",
"paddingHorizontal",
"paddingLeft",
"paddingRight",
"paddingTop",
"paddingVertical",
"position",
"resizeMode",
"right",
"rotation",
"scaleX",
"scaleY",
"shadowColor",
"shadowOffset",
"shadowOpacity",
"shadowRadius",
"textAlign",
"textDecorationColor",
"textDecorationLine",
"textDecorationStyle",
"tintColor",
"top",
"transform",
"transformMatrix",
"translateX",
"translateY",
"width",
"writingDirection"
8. 警告信息
在开发过程中,如果我们需要在界面中打印出信息,可以借助console.warn
打印出警告信息,而console.log
的信息需要开启debug模式,在控制台可见。
另外最常见的一个警告信息是提示你加上key属性,当我们遍历输出组件时,组件一定记得加上key属性,这样做能提高虚拟DOM Diff的效率。
9. 解决缓慢的导航器(Navigator)切换
Navigator
的动画是由JavaScript线程所控制的。想象一下“从右边推入”这个场景的切换:每一帧中,新的场景从右向左移动,从屏幕右边缘开始(不妨认为是320单位宽的的x轴偏移),最终移动到x轴偏移为0的屏幕位置。切换过程中的每一帧,JavaScript线程都需要发送一个新的x轴偏移量给主线程。如果JavaScript线程卡住了,它就无法处理这项事情,因而这一帧就无法更新,动画就被卡住了。
长远的解决方法,其中一部分是要允许基于JavaScript的动画从主线程分离。同样是上面的例子,我们可以在切换动画开始的时候计算出一个列表,其中包含所有的新的场景需要的x轴偏移量,然后一次发送到主线程以某种优化的方式执行。由于JavaScript线程已经从更新x轴偏移量给主线程这个职责中解脱了出来,因此JavaScript线程中的掉帧就不是什么大问题了 —— 用户将基本上不会意识到这个问题,因为用户的注意力会被流畅的切换动作所吸引。
不幸的是,这个方案还没有被实现。所以当前的解决方案是,在动画的进行过程中,利用InteractionManager
来选择性的渲染新场景所需的最小限度的内容。
InteractionManager.runAfterInteractions
的参数中包含一个回调,这个回调会在navigator切换动画结束的时候被触发。
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this.setState({renderPlaceholderOnly: false});
});
}
10. 全局变量
RN可以通过global
来设置全局变量,例如我们要把本地存储的方法挂载到全局:
global.storage = storage
之后直接使用storage
即可。
11. 调试
RN在开发菜单里提供了Debug JS Remotely
的选项,点击后会打开chrome,可以查看日志,断点调试。
另外还可以安装react-devtools进行样式调试。
更详细的调试文档,可以看这里。
12. WebView
RN自带了WebView
的支持,我们可以通过简单的封装,让它更易用,另外它除了支持url,还支持自定义的html。
13. 链接原生库
有一些库基于一些原生代码实现,你必须把这些文件添加到你的应用,否则应用会在你使用这些库的时候产生报错。
我们无需手动添加,通过react-native link
命令即可完成链接原生库。
14. Component命名
react声明组件时,第一个字母必须大写。
15. 字体引入
IOS上要使用自定义的字体,必须把字体文件拖到对应的Xcode工程里面,勾选Add to targets
和Create groups
,修改Info.plist
文件,添加属性Fonts provided by application
;
安卓上要使用自定义的字体,必须要把字体文件放在[project root]/android/app/src/main/assets/fonts/
目录下才能生效
16. icon解决方案
我们使用iconfont,然后进行了简单的封装,详细见此。
17. 使用ListView的正确姿势
我们在一次使用ListView
过程中,发现state不会改变,在GitHub上找到了同样问题的issues:this.state does't work at listView's renderRow。进而获得了一些使用ListView的正确姿势:适合动态列表数据,固化数据尽量不用,renderRow里尽量传数据,避免state判断,如需state,应该付给参数传入。
18. ScrollView
我们在使用ScrollView
的onScroll
方法的时候,有时会发现获取的值和我们的预期不一致,是因为ScrollView
默认每帧最多调用一次此回调函数,如果要增大调用的频率,可以用scrollEventThrottle
属性来控制。
19. 阴影
iOS上的阴影使用以下的属性:
shadowColor Sets the drop shadow color
shadowOffset {width: number, height: number}Sets the drop shadow offset
shadowOpacity numberSets the drop shadow opacity (multiplied by the color's alpha component)
shadowRadius numberSets the drop shadow blur radius
但注意如果给Image
组件添加阴影,不能把样式写在Image
的style,而需要包裹一层View来添加阴影样式。
Android上则不支持shadow*
的样式,只有elevation
仰角的属性来替代,但效果不太好,如果需要实现一致的效果,需要自己实现或者引入相关的库。
20. FlatList
FlatList
号称是ListView
的升级版,会有更好的体验、更高的效率,但目前这个组件还不稳定。使用过程有很多问题,例如首次加载会触发两次onEndReached
、必须设置height
属性,不然onEndReached
无法触发、下拉到底仍可下拉,并出现大片白屏等。
注:官方在0.48版本开始废弃
ListView
,推荐使用FlatList
或SectionList
,看来应该比较稳定了。
21. ref
任何组件都用一个ref
的属性,ref
是组件实例的引用,通过复制给this变量,可以在任意位置操作组件。
22. PureComponent
当props
或者state
改变的时候,会执行shouldComponentUpdate
方法来判断是否需要重新render
组建,我们平时在做页面的性能优化的时候,往往也是通过这一步来判断的。Component
默认的shouldComponentUpdate
返回的是true,如下:
shouldComponentUpdate(nextProps, nextState) {
return true;
}
而PureComponent
的shouldComponentUpdate
是这样的:
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState);
}
相当于PureComponent
帮我们判断如果props
或者state
没有改变的时候,就不重复render
,这对于纯展示组件,能节省不少比较的工作。
23. babelHelpers.objectDestructuringEmpty is not a function
在某些机子上遇到过一个如题的报错,具体看issue
原因是使用了如下的语法:
const {} = result
开发时尽量规范语法,避免写些无意义的语法。
24. IOS模拟器卡顿
别当心,很可能是你按到了快捷键,打开了慢动画的选项,关掉它就行了。
25. 快捷方式
IOS唤起调试菜单是⌘ + D
,刷新是⌘ + R
;
Android唤起调试菜单是⌘ + M
,刷新是R+ R
;
在真机上可以通过摇一摇唤起调试菜单。
26. LayoutAnimation
Animated
的接口一般会在JavaScript线程中计算出所需要的每一个关键帧,而LayoutAnimation
则利用了Core Animation
,使动画不会被JS线程和主线程的掉帧所影响。
注意:
LayoutAnimation
只工作在“一次性”的动画上("静态"动画) -- 如果动画可能会被中途取消,你还是需要使用Animated
。
27.本地存储的使用
这个问题琢磨了一段时间,还没有找到我想要的答案。情景大致是这样,一次访问某页面,通过AsyncStorage
保存了数据,第二次进入页面肯定希望render
中直接用AsyncStorage
中的本地数据,无需二次render
。但是AsyncStorage
是个异步函数,所以你即便在componentWillMount
调用,还是需要在render
后才能拿到数据,所以就会出现二次render
,即便componentWillMount
中用await
也无效,认真看了遍官方生命周期的文档,但并没有什么收获。目前的解决方案是用一个标志位控制,标志位为false
时出loading
,只有当拿到数据标志位为true
时才切真正的render
,但这种方案其实还是执行了两次render
,不过意外的是效果不错,看不出有闪动,甚至看不出有loading
过程。但如果你把关于本地存储的一系列判断逻辑是写在InteractionManager.runAfterInteractions
中,就会明显的看到loading
,打断点看了下,发现即便是两次render
,都发生在页面过场前,也就是屏幕还在上一页面的时候就在render
,而写在InteractionManager.runAfterInteractions
里,正是在执行过场或者过场执行完时发现,这里面的 state
变化反应到render
中就会在屏幕中被看到。当然这个问题我还是想继续关注下去,react-native
也有不少类似的issue
,最终还是希望能找到只需要一次render
的办法。
注:思路1(Redux
是无视生命周期的)
28.从原生页面如何跳转到指定RN页面
这里用到方法就是发送事件到JavaScript
,然后根据获取的参数,跳转相应的路由。
原生模块可以在没有被调用的情况下往JavaScript
发送事件通知。最简单的办法就是通过RCTDeviceEventEmitter
,这可以通过ReactContext
来获得对应的引用,像这样:
@ReactMethod
public void goPage(int pageid) {
System.out.println("########"+pageid+"########");
// failedCallback.invoke();
WritableMap params = Arguments.createMap();
params.putInt("name", pageid);
reactApplicationContextAction
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("test", params);
}
Javascript
通过DeviceEventEmitter
模块来监听事件,获取跳转信息,跳转至相应路由:
import {DeviceEventEmitter} from 'react-native'
componentWillMount(){
DeviceEventEmitter.addListener("test", (result) => {
let mainComponent = require(result.name);
this.setState({
content:mainComponent,
showModule:true
})
})
}
render(){
if(this.state.content){
route.push(this.state.content)
return null
}else{
return null
}
}
29.字体背景色
字体的背景色是会继承它父级的backgroundColor
,通常我们没有在意。但当你如果需要在字体上叠加一层蒙版也好、渐变也好,它们的颜色又恰好与你之前的背景色不一致,你就会发现字体的背景色凸显出来了,这时需要把字体的backgroundColor
设置为transparent
,这样才不会影响盖在它上面的层,当然遇到这样的问题还可能和你布局的先后顺序有关,通常使用absolute
应该排在后面,避免被后面的元素覆盖,zIndex
好像是只作用于同样是absolute
定义,absolute
和flex
之间无法使用zIndex
。
30.扩展性
使用原生方法(NativeModules
)
- IOS
想要创建一个iOS模块,只需要创建一个接口,实现RCTBridgeModule
协议,然后把你想在Javascript
中使用的任何方法用RCT_EXPORT_METHOD
包装。最后,再用RCT_EXPORT_MODULE
导出整个模块即可。
// Objective-C
#import "RCTBridgeModule.h"
@interface MyCustomModule : NSObject
@end
@implementation MyCustomModule
RCT_EXPORT_MODULE();
// Available as NativeModules.MyCustomModule.processString
RCT_EXPORT_METHOD(processString:(NSString *)input callback:(RCTResponseSenderBlock)callback)
{
callback(@[[input stringByReplacingOccurrencesOfString:@"Goodbye" withString:@"Hello"]]);
}
@end
// JavaScript
import React, {
Component,
} from 'react';
import {
NativeModules,
Text
} from 'react-native';
class Message extends Component {
constructor(props) {
super(props);
this.state = { text: 'Goodbye World.' };
}
componentDidMount() {
NativeModules.MyCustomModule.processString(this.state.text, (text) => {
this.setState({text});
});
}
render() {
return (
{this.state.text}
);
}
}
- Android
同样的,Android
也支持自定义扩展。仅仅是方法略有差异。
创建一个基础的安卓模块,需要先创建一个继承自ReactContentBaseJavaModule
的类,然后使用@ReactMethod
标注(Annotation
)来标记那些你希望通过Javascript来访问的方法。最后,需要在ReactPackage
中注册这个模块。
// Java
public class MyCustomModule extends ReactContextBaseJavaModule {
// Available as NativeModules.MyCustomModule.processString
@ReactMethod
public void processString(String input, Callback callback) {
callback.invoke(input.replace("Goodbye", "Hello"));
}
}
// JavaScript
import React, {
Component,
} from 'react';
import {
NativeModules,
Text
} from 'react-native';
class Message extends Component {
constructor(props) {
super(props);
this.state = { text: 'Goodbye World.' };
},
componentDidMount() {
NativeModules.MyCustomModule.processString(this.state.text, (text) => {
this.setState({text});
});
}
render() {
return (
{this.state.text}
);
}
}
使用原生页面(requireNativeComponent
)
- IOS
若想自定义iOS View
,可以这样来做:首先继承RCTViewManager
类,然后实现一个-(UIView *)view
方法,并且使用RCT_EXPORT_VIEW_PROPERTY
宏导出属性。最后用一个Javascript
文件连接并进行包装。
// Objective-C
#import "RCTViewManager.h"
@interface MyCustomViewManager : RCTViewManager
@end
@implementation MyCustomViewManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[MyCustomView alloc] init];
}
RCT_EXPORT_VIEW_PROPERTY(myCustomProperty, NSString);
@end
// JavaScript
import React, {
Component,
} from 'react';
import PropTypes from 'prop-types';
import { requireNativeComponent } from 'react-native';
var NativeMyCustomView = requireNativeComponent('MyCustomView', MyCustomView);
export default class MyCustomView extends Component {
static propTypes = {
myCustomProperty: PropTypes.oneOf(['a', 'b']),
};
render() {
return ;
}
}
- Android
创建自定义的Android View
,首先定义一个继承自SimpleViewManager
的类,并实现createViewInstance
和getName
方法,然后使用@ReactProp
标注导出属性,最后用一个Javascript
文件连接并进行包装。
// Java
public class MyCustomViewManager extends SimpleViewManager {
@Override
public String getName() {
return "MyCustomView";
}
@Override
protected MyCustomView createViewInstance(ThemedReactContext reactContext) {
return new MyCustomView(reactContext);
}
@ReactProp(name = "myCustomProperty")
public void setMyCustomProperty(MyCustomView view, String value) {
view.setMyCustomProperty(value);
}
}
// JavaScript
import React, {
Component,
requireNativeComponent
} from 'react-native';
var NativeMyCustomView = requireNativeComponent('MyCustomView', MyCustomView);
export default class MyCustomView extends Component {
static propTypes = {
myCustomProperty: React.PropTypes.oneOf(['a', 'b']),
};
render() {
return ;
}
}
更多(使用原生UI、同个页面RN与Native的相互嵌套等)
更多和原生通信的内容可以看官网文档:英文、中文
31.IOS真机打包
如何没有IOS
开发者账号,一个项目只允许最多在三台设备上打包,而且过期时间只有7天,另外无法移除打包过的机子的mac地址,意思就是我在A机子装过,就用掉一个名额,没法把这个名额让出来了。这样导致我们在铺开测试、给大家体验时遇到了瓶颈。这时候最好的方案是有一个企业账号,可以打出一个企业包,在任何机子安装,如果只有开发者账号,那也只能在100台设备安装,开启和关闭权限都需要到开发者网站操作,收回权限还需要给Apple发邮件。打完包,我推荐用fir平台托管应用,只要把生成的页面或者二维码发给大家即可,方便、快捷,另外还支持权限、密码的设置,实名制后每天有一百次的下载额度,其实也是足够用了。
32.如何实现回退后刷新上个页面
刷新上个页面,说白了就是传参。目前用的navigator
,只有push
能传参,pop
并没有,这样如何做到页面回退能让上一个页面感知呢?我尝试了几个办法:
-
Redux
通过redux
的store
,简单粗暴,没啥好说 -
DeviceEventEmitter
第一个页面监听,回退的时候触发,其实就是个简单的观察者模式,代码大致如下:
//A页面
import {
AppRegistry,
StyleSheet,
Text,
View,
DeviceEventEmitter
} form 'react-native';
componentDidMount() {
this.subscription = DeviceEventEmitter.addListener('userNameDidChange',(userName) =>{
console.warn(userName);
})
}
componentWillUnmount() {
// 移除
this.subscription.remove();
}
//B页面,在回退前
DeviceEventEmitter.emit('userNameDidChange', '通知来了');
-
callback
在A界面跳到B界面时,带上回调参数,如:
this.props.navigator.push({‘id’:’b’,’callback’:this.refreshAAvatar}
然后在你回退前执行callback
即可
33. 在初始化bundle时如何传参
在注册bundle时传参有什么用呢?可以实现跳转到特定页面。
来看看IOS和Android分别是怎么实现:
//IOS initialProps就是给RN的参数
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"MyAwesomeApp"
initialProperties:initialProps
launchOptions:launchOptions];
//Android
Bundle initialProps = new Bundle();
initialProps.putString("myKey", "myValue");
mReactRootView.startReactApplication(mReactInstanceManager, "MyAwesomeApp", initialProps);
34. 使用React Navigation实现App唤醒功能
详见官网文档