首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  React

ReactNative 开发笔记(持续更新...)

  •  
  •   kovli · 29 天前 · 166 次点击

    本文均为 RN 开发过程中遇到的问题、坑点的分析及解决方案,各问题点之间无关联,希望能帮助读者少走弯路,持续更新中... ( 2018 年 12 月 19 日更新)

    原文链接: http://www.kovli.com/2018/06/25/rn-anything/

    作者:Kovli

    - ReactNative 输入框 TextInput 点击弹起键盘,如果键盘遮挡了重要位置,如何让界面自动跟随键盘调整?

    使用这个组件KeyboardAvoidingView

    本组件用于解决一个常见的尴尬问题:手机上弹出的键盘常常会挡住当前的视图。本组件可以自动根据键盘的位置,调整自身的 position 或底部的 padding,以避免被遮挡。

    解决方案:参考如下例子

          <ScrollView style={styles.container}>
            <KeyboardAvoidingView behavior="position" keyboardVerticalOffset={64}>
              ...
              <TextInput />
              ...
            </KeyboardAvoidingView>
          </ScrollView>
    
    
    

    - ReactNative 输入框 TextInput 点击弹起键盘,然后点击其他子组件,例如点击提交按钮,会先把键盘收起,再次点击提交按钮才响应提交按钮,得点击两次,如何做到点击提交按钮的同时收起键盘并响应按钮?

    这个问题关键在ScrollViewkeyboardShouldPersistTaps属性 ,首先TextInput的特殊性(有键盘弹起)决定了其最好包裹在 ScrollView 里,其次如果当前界面有软键盘,那么点击scrollview后是否收起键盘,取决于keyboardShouldPersistTaps属性的设置。(译注:很多人反应TextInput无法自动失去焦点 /需要点击多次切换到其他组件等等问题,其关键都是需要将TextInput放到ScrollView中再设置本属性)

    • 'never'(默认值),点击 TextInput 以外的子组件会使当前的软键盘收起。此时子元素不会收到点击事件。
    • 'always',键盘不会自动收起,ScrollView 也不会捕捉点击事件,但子组件可以捕获。
    • 'handled',当点击事件被子组件捕获时,键盘不会自动收起。这样切换 TextInput 时键盘可以保持状态。多数带有 TextInput 的情况下你应该选择此项。
    • false,已过期,请使用'never'代替。
    • true,已过期,请使用'always'代替。

    解决方案:看如下例子

          <ScrollView style={styles.container}
                      keyboardShouldPersistTaps="handled">
            <TextInput />
           ...
          </ScrollView>
    
        //按钮点击事件注意收起键盘
          _checkAndSubmit = () => {
            Keyboard.dismiss();
          };
    

    - ReactNative 本地图片如何获取其 base64 编码?(一般指采用<Image source={require('./icon.png'.../>这类相对路径地址的图片资源如何获取到绝对路径)

    关键是要获取到本地图片的 uri,用到了Image.resolveAssetSource方法,ImageEditor.cropImage方法和ImageStore.getBase64ForTag方法,具体可以查询官方文档

    解决方案:看如下代码

          import item from '../../images/avator_upload_icon.png';
    
          const info = Image.resolveAssetSource(item);
    
          ImageEditor.cropImage(info.uri, {
            size: {
              width: 126,
              height: 126
            },
            resizeMode: 'cover'
          }, uri => {
            ImageStore.getBase64ForTag(uri, base64ImageData => {
              // 获取图片字节码的 base64 字符串
              this.setState({
                avatarBase64: base64ImageData
              });
            }, err => {
              console.warn("ImageStoreError" + JSON.stringify(err));
            });
          }, err => {
            console.warn("ImageEditorError" + JSON.stringify(err));
    
          });
    
    

    - ReactNative 如何读取 iOS 沙盒里的图片?

    解决方案:看如下代码

          let RNFS = require('react-native-fs');
            <Image
              style={{width:100, height:100}}
              source={{uri: 'file://' + RNFS.DocumentDirectoryPath + '/myAwesomeSubDir/my.png', scale:1}}
    
    
    

    - ReactNative 如何做到图片宽度不变,宽高保持比例,高度自动调整。

    RN 图片均需要指定宽高才会显示,如果图片数据的宽高不定,但又希望宽度保持不变、不同图片的高度根据比例动态变化,就需要用到下面这个库,业务场景常用于文章、商品详情的多图展示。

    解决方案:使用react-native-scalable-image

    - navigor 无法使用的解决办法

    从 0.44 版本开始,Navigator 被从 react native 的核心组件库中剥离到了一个名为react-native-deprecated-custom-components的单独模块中。如果你需要继续使用 Navigator,则需要先npm i facebookarchive/react-native-custom-components安装,然后从这个模块中 import,即import { Navigator } from 'react-native-deprecated-custom-components'

    如果报错如下参考下面的解决方案

    React-Native – undefined is not an object (“ evaluating _react3.default.PropTypes.shape ”)

    解决方案

    如果已经安装了,先卸载npm uninstall --save react-native-deprecated-custom-components

    用下面的命令安装 npm install --save https://github.com/facebookarchive/react-native-custom-components.git

    在我们使用 Navigator 的 js 文件中加入下面这个导入包就可以了。

    import { Navigator } from'react-native-deprecated-custom-components';(注意最后有一个分号)

    就可以正常使用 Navigator 组件了。

    - ReactNative 开发的 APP 启动闪白屏问题

    由于处理 JS 需要时间,APP 启动会出现一闪而过白屏,可以通过启动页延迟加载方法来避免这类白屏,可以用下面的库 解决方案react-native-splash-screen

    - ReactNative 如何做到无感热更新

    无论是整包热更新还是差量热更新,均需要最终替换 JSBundle 等文件来完成更新过程,实现原理是 js 来控制启动页的消失时间,等原生把 bundle 包下载(或合并成新 bundle 包)解压到目录以后,通知 js 消失启动页,由于热更新时间一般很短,建议使用差量热更新,一秒左右,所以用户等启动页消失后看到的就是最新的版本。 解决方案(以整包更新为例):

    1. 原生端完成更新及刷新操作,注意里面的[_bridge reload]
    //前往更新 js 包
    RCT_EXPORT_METHOD(gotoUpdateJS:(NSString *)jsUrl andResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){
      if (!jsUrl) {
        return;
      }
      
      //jsbundle 更新采用静默更新
      //更新
      NSLog(@"jsbundleUrl is : %@",  jsUrl);
      [[LJFileHelper shared] downloadFileWithURLString:jsUrl finish:^(NSInteger status, id data) {
        if(status == 1){
          NSLog(@"下载完成");
          NSError *error;
          NSString *filePath = (NSString *)data;
          NSString *desPath = [NSString stringWithFormat:@"%@",NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]];
          [SSZipArchive unzipFileAtPath:filePath toDestination:desPath overwrite:YES password:nil error:&error];
          if(!error){
            [_bridge reload];
    
            resolve([NSNumber numberWithBool:true]);
            NSLog(@"解压成功");
            
          }else{
            resolve([NSNumber numberWithBool:false]);
            NSLog(@"解压失败");
          }
        }
      }];
    
      reject = nil;
    }
    
    
    1. JS 端
    // 原生端通过回调结果通知 JS 热更新情况,JS 端
    UpdateModule.gotoUpdateJS(jsUrl).then(resp => {
    
        if ( resp ) {
           // 成功更新通知隐藏启动页
           DeviceEventEmitter.emit("hide_loading_page",'hide');
        } else {
           // 出问题也要隐藏启动页,用户继续使用旧版本
           DeviceEventEmitter.emit("hide_loading_page",'hide');
           // 其他处理
        }
    
    });
    
    1. 启动页消失,用户看到的是新版 APP
        async componentWillMount() {
    
            this.subscription = DeviceEventEmitter.addListener("hide_loading_page", this.hideLoadingPage);
            appUpdateModule.updateJs();
    
    
        }
    
        hideLoadingPage = ()=> {
            SplashScreen.hide();
        };
    
    

    注意做好容错,例如弱网无网环境下的处理,热更新失败下次保证再次热更新的处理,热更新时间把控,超过时间下次再 reload,是否将热更新 reload 权利交给用户等等都可以扩展。

    - ReactNative 如何取消部分警告

    debug 模式下调试经常会有黄色的警告,有些警告可能是短时间不需要处理,通过下面的解决方法能忽略部分警告提示

    解决方案:使用console.ignoredYellowBox

    import { AppRegistry } from 'react-native';
    import './app/Common/SetTheme'
    import './app/Common/Global'
    
    
    import App from './App';
    
    console.ignoredYellowBox = ['Warning: BackAndroid is deprecated.  Please use BackHandler instead.',
        'source.uri should not be an empty string','Remote debugger is in a background tab which',
        'Setting a timer',
        'Encountered two children with the same key,',
        'Attempt to read an array index',
    ];
    
    AppRegistry.registerComponent('ReactNativeTemplate', () => App);
    

    - ReactNative 开发遇到 android 网络图片显示不出来的问题

    开发过程中有时会遇到 iOS 图片正常显示,但是安卓却只能显示部分网络图片,造成这个的原因有多种,参考下面的解决方案。

    解决方案

    1. 安卓增加 resizeMethod 属性并设置为 resize
    <Image style={styles.imageStyle} source={{uri: itemInfo.imageUrl || ''}} resizeMethod={'resize'}/>
    

    resizeMethod 官方解释

    resizeMethod  enum('auto', 'resize', 'scale') 
    
    当图片实际尺寸和容器样式尺寸不一致时,决定以怎样的策略来调整图片的尺寸。默认值为 auto。
    
    auto:使用启发式算法来在 resize 和 scale 中自动决定。
    
    resize: 在图片解码之前,使用软件算法对其在内存中的数据进行修改。当图片尺寸比容器尺寸大得多时,应该优先使用此选项。
    
    scale:对图片进行缩放。和 resize 相比,scale 速度更快(一般有硬件加速),而且图片质量更优。在图片尺寸比容器尺寸小或者只是稍大一点时,应该优先使用此选项。
    
    关于 resize 和 scale 的详细说明请参考 http://frescolib.org/docs/resizing-rotating.html.
    
    1. 如果是 FlatList 或 ScrollView 等包裹图片,尝试设置

    removeClippedSubviews={true}//ios set false

    1. 如果还是有问题,尝试配合react-native-image-progress

      还可以谨慎尝试使用react-native-fast-image

    - ReactNative 判断及监控网络情况方法总结

    提前获取用户的网络情况很有必要,RN 主要靠 NetInfo 来获取网络状态,不过随着 RN 版本的更新也有一些变化。 解决方案:

    1. 较新的 RN 版本(大概是 0.50 及以上版本)
        this.queryConfig();
    
    
        queryConfig = ()=> {
            this.listener = NetInfo.addEventListener('connectionChange', this._netChange);
        };
        
            // 网络发生变化时
        _netChange = async(info)=> {
            const {
                type,
                //effectiveType
            } = info;
            const netCanUse = !(type === 'none' || type === 'unknown' || type === 'UNKNOWN' || type === 'NONE');
            if (!netCanUse) {
                this.setState({
                    isNetError : true
                });
                this.alertNetError(); //或者其他通知形式
    
            } else {
                try {
                    // 注意这里的 await 语句,其所在的函数必须有 async 关键字声明
                    let response = await fetch(CONFIG_URL);
                    let responseJson = await response.json();
                    const configData = responseJson.result;
                    if (response && configData) {
                        this.setState({
                            is_show_tip: configData.is_show_tip,
                            app_bg: CONFIG_HOST + configData.app_bg,
                            jumpUrl: configData.url,
                            isGetConfigData: true
                        }, () => {
                            SplashScreen.hide();
                        })
                    } else {
                        // 错误码也去壳
                        if ( responseJson.code === 400 ) {
                            this.setState({
                                isGetConfigData: true
                            }, () => {
                                SplashScreen.hide();
                            })
                        } else {
                            this.setState({
                                isGetConfigData: false
                            }, () => {
                                SplashScreen.hide();
                            })
                        }
                    }
    
                } catch (error) {
                    console.log('queryConfig error:' + error);
                    this.setState({
                        isGetConfigData: true
                    }, () => {
                        SplashScreen.hide();
                    })
                }
    
            }
        };
        
        
        
        
            alertNetError = () => {
            setTimeout(()=> {
                SplashScreen.hide();
    
            }, 1000);
    
            if ( ! this.state.is_show_tip &&  this.state.isGetConfigData ) {
                return
            } else {
                Alert.alert(
                    'NetworkDisconnected',
                    '',
                    [
                        {text: 'NetworkDisconnected_OK', onPress: () => {
                            this.checkNetState();
                        }},
                    ],
                    {cancelable: false}
                );        }
    
    
        };
        
        
        
        
            checkNetState = () => {
            NetInfo.isConnected.fetch().done((isConnected) => {
                if ( !isConnected ) {
                    this.alertNetError();
                } else {
                    this.queryConfig();
                }
            });
    
        };
    
    
    1. 老版本
        async componentWillMount() {
            this.queryConfig();
        }
    
        checkNetState = () => {
            NetInfo.isConnected.fetch().done((isConnected) => {
                console.log('111Then, is ' + (isConnected ? 'online' : 'offline'));
                if (!isConnected) {
                    this.alertNetError();
                } else {
                    this.queryConfig();
                }
            });
    
        };
    
        alertNetError = () => {
            setTimeout(()=> {
                SplashScreen.hide();
    
            }, 1000);
            console.log('111111');
    
            if (!this.state.is_show_tip && this.state.isGetConfigData) {
                console.log('222222');
    
                return
            } else {
                console.log('33333');
    
                Alert.alert(
                    'NetworkDisconnected',
                    '',
                    [
                        {
                            text: 'NetworkDisconnected_OK', onPress: () => {
                            this.checkNetState();
                        }
                        },
                    ],
                    {cancelable: false}
                );
            }
    
    
        };
    
    
        queryConfig = ()=> {
           
            NetInfo.isConnected.addEventListener(
                'connectionChange',
                this._netChange
            );
    
        };
    
    
        // 网络发生变化时
        _netChange = async(isConnected)=> {
            console.log('Then, is ' + (isConnected ? 'online' : 'offline'));
    
        
            if (!isConnected) {
                console.log('666');
    
                this.setState({
                    isNetError: true
                });
                this.alertNetError();
    
            } else {
                try {
                    // 注意这里的 await 语句,其所在的函数必须有 async 关键字声明
                    let response = await fetch(CONFIG_URL);
                    let responseJson = await response.json();
                    const configData = responseJson.result;
                    if (response && configData) {
                        this.setState({
                            is_show_tip: configData.is_show_tip,
                            app_bg: CONFIG_HOST + configData.app_bg,
                            jumpUrl: configData.url,
                            isGetConfigData: true
                        }, () => {
                            SplashScreen.hide();
                            this.componentNext();
                        })
                    } else {
                        this.setState({
                            isGetConfigData: false
                        }, () => {
                            SplashScreen.hide();
                            this.componentNext();
                        })
                    }
    
                } catch (error) {
                    console.log('queryConfig error:' + error);
                    this.setState({
                        isGetConfigData: true
                    }, () => {
                        SplashScreen.hide();
                        this.componentNext();
                    })
                }
    
            }
        };
    

    - ReactNative 版本升级后报错有废弃代码的快速解决方法

    使用第三方库或者老版本升级时会遇到报错提示某些方法被废弃,这时候寻找和替换要花不少时间,而且还容易漏掉。

    解决方案: 根据报错信息,搜索废弃的代码,例如

    报错提示:Use viewPropTypes instead of View.propTypes.

    搜索命令:grep -r 'View.propTypes' .

    替换搜索出来的代码即可。

    这是用于查找项目里的错误或者被废弃的代码的好方法

    - 解决 ReactNative 的 TextInput 在 0.55 中文无法输入的问题

    此问题主要体现在 iOS 中文输入法无法输入汉字,是 0.55 版 RN 的一个 bug

    解决方案:使用下面的MyTextInput替换原TextInput

    import React from 'react';
    import { TextInput as Input } from 'react-native';
    
    export default class MyTextInput extends React.Component {
        static defaultProps = {
            onFocus: () => { },
        };
    
        constructor(props) {
            super(props);
    
            this.state = {
                value: this.props.value,
                refresh: false,
            };
        }
    
        shouldComponentUpdate(nextProps, nextState) {
            if (this.state.value !== nextState.value) {
                return false;
            }
    
            return true;
        }
    
        componentDidUpdate(prevProps) {
            if (prevProps.value !== this.props.value && this.props.value === '') {
                this.setState({ value: '', refresh: true }, () => this.setState({ refresh: false }));
            }
        }
    
        focus = (e) => {
            this.input.focus();
        };
    
        onFocus = (e) => {
            this.input.focus();
    
            this.props.onFocus();
        };
    
        render() {
            if (this.state.refresh) {
                return null;
            }
    
            return (
                <Input
                    {...this.props}
                    ref={(ref) => { this.input = ref; }}
                    value={this.state.value}
                    onFocus={this.onFocus}
                />
            );
        }
    }
    

    ReactNative 集成第三方 DEMO 编译时遇到 RCTSRWebSocket 错误的解决方法

    报错信息如下

    Ignoring return value of function declared with warn_unused_result attribute
    

    解决方案

    StackOverFlow 上的解决方法:

    在 navigator 双击 RCTWebSocket project,移除 build settings > custom compiler 下的 flags
    

    版权声明:

    转载时请注明作者Kovli以及本文地址: http://www.kovli.com/2018/06/25/rn-anything/


    目前尚无回复
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1194 人在线   最高记录 4236   ·  
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.2 · 18ms · UTC 23:47 · PVG 07:47 · LAX 15:47 · JFK 18:47
    ♥ Do have faith in what you're doing.
    沪ICP备16043287号-1