美文网首页
React Native开发小贴士

React Native开发小贴士

作者: 哪吒闹海全靠浪 | 来源:发表于2017-10-19 16:27 被阅读0次

    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, TouchableOpacityTouchableWithoutFeedback。因为这几个组件的功能和使用方法基本类似,只是 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,
        });
    }
    
    <View  
      {...this._panResponder.panHandlers}
    />
    

    但这个手势库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 targetsCreate 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

    我们在使用ScrollViewonScroll方法的时候,有时会发现获取的值和我们的预期不一致,是因为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,推荐使用FlatListSectionList,看来应该比较稳定了。

    21. ref

    任何组件都用一个ref的属性,ref是组件实例的引用,通过复制给this变量,可以在任意位置操作组件。

    22. PureComponent

    props或者state改变的时候,会执行shouldComponentUpdate方法来判断是否需要重新render组建,我们平时在做页面的性能优化的时候,往往也是通过这一步来判断的。Component默认的shouldComponentUpdate返回的是true,如下:

    shouldComponentUpdate(nextProps, nextState) {
      return true;
    }
    

    PureComponentshouldComponentUpdate是这样的:

    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定义,absoluteflex之间无法使用zIndex

    30.扩展性

    使用原生方法(NativeModules)

    • IOS

    想要创建一个iOS模块,只需要创建一个接口,实现RCTBridgeModule协议,然后把你想在Javascript中使用的任何方法用RCT_EXPORT_METHOD包装。最后,再用RCT_EXPORT_MODULE导出整个模块即可。

    // Objective-C
    
    #import "RCTBridgeModule.h"
    
    @interface MyCustomModule : NSObject <RCTBridgeModule>
    @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 (
          <Text>{this.state.text}</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 (
          <Text>{this.state.text}</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 <NativeMyCustomView {...this.props} />;
      }
    }
    
    • Android

    创建自定义的Android View,首先定义一个继承自SimpleViewManager的类,并实现createViewInstancegetName方法,然后使用@ReactProp标注导出属性,最后用一个Javascript文件连接并进行包装。

    // Java
    
    public class MyCustomViewManager extends SimpleViewManager<MyCustomView> {
      @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 <NativeMyCustomView {...this.props} />;
      }
    }
    

    更多(使用原生UI、同个页面RN与Native的相互嵌套等)

    更多和原生通信的内容可以看官网文档:英文中文

    31.IOS真机打包

    如何没有IOS开发者账号,一个项目只允许最多在三台设备上打包,而且过期时间只有7天,另外无法移除打包过的机子的mac地址,意思就是我在A机子装过,就用掉一个名额,没法把这个名额让出来了。这样导致我们在铺开测试、给大家体验时遇到了瓶颈。这时候最好的方案是有一个企业账号,可以打出一个企业包,在任何机子安装,如果只有开发者账号,那也只能在100台设备安装,开启和关闭权限都需要到开发者网站操作,收回权限还需要给Apple发邮件。打完包,我推荐用fir平台托管应用,只要把生成的页面或者二维码发给大家即可,方便、快捷,另外还支持权限、密码的设置,实名制后每天有一百次的下载额度,其实也是足够用了。

    32.如何实现回退后刷新上个页面

    刷新上个页面,说白了就是传参。目前用的navigator,只有push能传参,pop并没有,这样如何做到页面回退能让上一个页面感知呢?我尝试了几个办法:

    • Redux
      通过reduxstore,简单粗暴,没啥好说
    • 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唤醒功能

    详见官网文档

    相关文章

      网友评论

          本文标题:React Native开发小贴士

          本文链接:https://www.haomeiwen.com/subject/mopeuxtx.html