RN项目问题总结梳理

问题1

问题描述:TextInput组件在页面底部时,弹出键盘时遮挡TextInput组件,用户无法正常输入内容
分析解决:弹出键盘浮在页面布局之上占用了一部分布局控件,可以监听键盘的弹出和隐藏事件,实现动态调整页面布局。

//页面装载时
componentWillMount() {
    //监听键盘弹出事件
    this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShowHandler.bind(this));
    //监听键盘隐藏事件
    this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHideHandler.bind(this));
}

//页面移除时
componentWillUnmount() {
    //卸载键盘弹出事件监听
    if (this.keyboardDidShowListener != null) {
        this.keyboardDidShowListener.remove();
    }
    //卸载键盘隐藏事件监听
    if (this.keyboardDidHideListener != null) {
        this.keyboardDidHideListener.remove();
    }
}
//自定义键盘事件处理
//键盘弹出事件响应
keyboardDidShowHandler(event) {
    this.setState({keyboardShow: true});
    console.log(event.endCoordinates.height);
}

//键盘隐藏事件响应
keyboardDidHideHandler(event) {
    this.setState({keyboardShow: false});
}

//强制隐藏键盘
dissmissKeyboard() {
    Keyboard.dismiss();
    console.log("输入框当前焦点状态:" + this.refs.bottomInput.isFocused());
}

在render()方法中,通过keyboardShow实现动态布局

{this.state.keyboardShow ? null :
    <Image style={styles.devImg}
           source={require('../../../res/img/device/dev_solar_add.png')}/>
}
<Text style={this.state.keyboardShow ? [styles.textTipMain,  {marginBottom:autoHeight(19)}] :styles.textTipMain}>{getString('solar_connect_input_model')}</Text>

问题2

问题描述:在某些情景下需要监听当前页面的物理返回键,例如双击back退出应用,或者某一页按下back键返回特定页面
分析解决:RN中的页面是通过组件的方式实现的,各页面之间的通过路由的方式连接起来,单纯在当前页面监听hardwareBackPress事件,不止会监听到当前页面的物理返回键事件,也会监听到该页面所有上层页面的物理返回键事件。
网上有很多说的是通过路由长度navigator.getCurrentRoutes().length 判断当前所在页面,尝试多次无果,之后找到了新版本react-navigation的当前页面物理返回键监听的正确方式:

class ScreenWithCustomBackBehavior extends Component {
    componentDidMount() {
        BackHandler.addEventListener('hardwareBackPress',
            this.onBackButtonPressAndroid);
    }

    componentWillUnmount() {
        BackHandler.removeEventListener('hardwareBackPress',
            this.onBackButtonPressAndroid);
    }

    onBackButtonPressAndroid = () => {
        if (this.props.navigation.isFocused()) {
            if (this.lastBackPressed && this.lastBackPressed + 2000 >= Date.now()) {
                //最近2秒内按过back键,可以退出应用。
                return false;
            }
            this.lastBackPressed = Date.now();
            ToastAndroid.show('再按一次退出应用', ToastAndroid.SHORT);
            return true;
        }
    }
}

有些版本在componentWillUnmount里执行removeEventListener并没有用,还是会监听到,所以这个时候可以换一种写法:

componentDidMount() {
    this.backHandler = BackHandler.addEventListener('hardwareBackPress',
        this.onBackButtonPressAndroid);
}

componentWillUnmount() {
    this.backHandler&&this.backHandler.remove();
}

问题3

问题描述:RN里边多语言适配一般用react-native-i18n第三方多语言库,具体使用参考https://github.com/AlexanderZaytsev/react-native-i18n,App多语言通常有一个需求就是切换语言,如果没有杀死应用进程,切换系统语言之后,App仍然后展示切换之前的语言。
分析解决:切换系统语言之后,App仍然显示为切换之前的语言,很可能是当前语言设置没有生效或者是没有进行语言的重新获取。通过原生方式监听切换系统语言的广播并发送消息给RN,然后再RN应用入口处接收系统语言切换的消息,并设置当前语言为切换后的系统语言:

//原生系统语言切换广播监听
public class LocalReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(Intent.ACTION_LOCALE_CHANGED)) {
reactContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("local_changed",null);
        }
    }
}

RN入口接收消息

componentWillMount() {
    console.log('---componentWillMount---');
this.subLocalChangedListener = DeviceEventEmitter.addListener("local_changed", () => {
    //每次进入应用前台检查语言
    if (ObjectUtil.isEmpty(I18n.locale)) {
        getLanguages()
            .then(languages => {
                console.log('当前语言' + languages);
                if (languages.toString().match(I18nUtils.ZH)) {
                    I18n.locale = I18nUtils.ZH;
                } else {
                    I18n.locale = I18nUtils.EN;
                }
            })
            .catch((error) => {
                    console.log("获取语言失败" + error);
                }
            );
        }
    });
}

componentWillUnmount() {
    console.log('---componentWillUnmount---');
this.susubLocalChangedListener .remove();
}

问题4

问题描述:手势滑动事件处理粒度太粗,导致部分手机上当前页面点击事件无法响应
分析解决:在处理首页预留空间滑动事件时,在onStartShouldSetPanResponder事件时就成为事件的响应者,导致该组件成为所有触摸事件(包括点击事件)的响应者,而该组件又不能很好的处理自己所需触摸事件之外的其他事件,默认是不做任何响应,这就导致了页面点击事件无响应。详细分析RN里边触摸事件的分发处理机制,逐步细化处理触摸事件的响应,不要在一开始就响应,当满足特定需求之后,例如滑动时滑动长度超过特定长度之后再处理,这样既不会影响其他组件的默认事件处理机制,也能够处理特定情况的手势事件。

componentWillMount() {
    this._panResponder = PanResponder.create({
        // 要求成为响应者:
        onStartShouldSetPanResponder: (evt, gestureState) => false,
        onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
        onMoveShouldSetPanResponder: (evt, gestureState) => false,
        onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
            if (Math.abs(gestureState.dx) < this.thresholdMin && Math.abs(gestureState.dy) < this.thresholdMin) {
                return false;
            }else{
                if ((this.show && gestureState.dy < 0) || (!this.show && gestureState.dy > 0)) {
                    return true;
                } else {
                    return false;
                }
            }
        },

        onPanResponderGrant: (evt, gestureState) => {
            // 开始手势操作。给用户一些视觉反馈,让他们知道发生了什么事情!
            console.log('onPanResponderGrant');
            // gestureState.{x,y} 现在会被设置为0
        },
        onPanResponderMove: (evt, gestureState) => {
            // 最近一次的移动距离为gestureState.move{X,Y}
            console.log('onPanResponderMove');
            // 从成为响应者开始时的累计手势移动距离为gestureState.d{x,y}
            if ((this.show && gestureState.dy < 0) || (!this.show && gestureState.dy > 0)) {
                }
            }
        },
        onPanResponderTerminationRequest: (evt, gestureState) => true,
        onPanResponderRelease: (evt, gestureState) => {
            // 用户放开了所有的触摸点,且此时视图已经成为了响应者。
            console.log('onPanResponderRelease');
            // 一般来说这意味着一个手势操作已经成功完
        },
        onPanResponderTerminate: (evt, gestureState) => {
            // 另一个组件已经成为了新的响应者,所以当前手势将被取消。
            console.log('onPanResponderTerminate');
        },
        onShouldBlockNativeResponder: (evt, gestureState) => {
            // 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者
            // 默认返回true。目前暂时只支持android。
            return true;
        },
    });
}

问题5

问题描述:FlatList列表加载圈莫名消失,场景:网络慢时导致首页获取不到设备列表,首页为空,不能下拉刷新。
分析解决:这个问题真的是莫名其妙,我第一感觉是怀疑逻辑处理不严谨,开始按照网络请求-正常-异常展示各种情况梳理,顺便整理了下逻辑,没有问题,就把它定义为莫名消失了。记得刚开始简单列表demo的时候就不存在这个问题,和demo对比,转换网络请求的方式,就差把demo代码全部替换过来了。正常逻辑怎么也想不到,把布局调整成最简单的了,终于在网络请求失败时加载圈也不会消失了,那么问题就是出在布局上,也不是布局有误,就是给FlatList的父布局设置了FlexBox布局的属性就出现这个问题了,如下:

container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    alignSelf: 'center',
}

去掉这三个属性,如下,其他代码逻辑复原,问题解决了。

container: {
    flex: 1,
}

问题6

问题描述:高版本手机通知栏小图标处理,在部分5.0或7.0手机的消息通知栏不仅会显示消息图标,还会显示一个非常小的应用通知图标,和其他应用对比发现默认显示效果不明显
分析解决:在部分高版本手机上,如果没有设置应用通知图标,系统会默认用应用的icon缩略一个小的通知图标(图标有颜色的部分会填充为白色,导致显示不出来),除非自己指定通知小图标。
找UI出了一个透明背景的icon(因为有颜色的部分会填充为白色,影响图标原有显示效果),指定为通知小图标,这样通知小图标就能显示出正常效果。

问题7

问题描述:编译版本不一致,例如targetSdk,google-supprot,firebase等版本不统一导致编译不通过问题

报错1:Error:Execution failed for task ':app:transformClassesWithDexForDebug'.
com.android.build.api.transform.TransformException: com.android.ide.common.process.ProcessException:
报错2:Program type already present: android.support.v4.app.INotificationSideChannel
报错3:com.android.builder.internal.aapt.v2.Aapt2Exception: AAPT2 error: check logs for details
报错4:unable to find attribute android:fontVariationSettings and android:ttcIndex
报错5:java.util.zip.ZipException: duplicate entry
报错6:Android dependency 'com.google.android.gms:play-services-basement' has different version for the compile (16.0.1) and runtime (16.1.0) classpath. You should manually set the same version via DependencyResolution

分析解决:针对以上系列问题,花费了两天半时间,一直在排查这一系列问题,网上搜出的解决方案五花八门,但是都没有完全解决。之后回归问题初始是由于版本冲突/重复依赖第三方包导致的重复依赖或者依赖冲突。从第三方包依赖冲突入手,最容易出现依赖冲突的就是com.android.support组和com.google.firebase组下的包,参考Stack Overflow论坛里解决com.android.support包依赖冲突的解决方法,在project的build.gradle里配置com.android.support的统一版本,同时配置com.google.firebase和com.google.android.gms的同一版本,终于编译通过了。

allprojects {
    repositories {
        ...
        }
        maven {
                 url 'https://maven.google.com/'
                 name 'Google'
        }
        configurations.all {
            resolutionStrategy.eachDependency { DependencyResolveDetails details ->
                def requested = details.requested
                if (requested.group == 'com.google.android.gms') {
                    details.useVersion '12.0.1'
                }
                if (requested.group == 'com.google.firebase') {
                    details.useVersion '12.0.1'
                }
            }
        }
    }
    subprojects {
        project.configurations.all {
            resolutionStrategy.eachDependency { details ->
                if (details.requested.group == 'com.android.support'
                        && !details.requested.name.contains('multidex') ) {
                    details.useVersion "26.1.0"
                }
            }
        }
    }
}

问题8

问题描述:app启动后,先出现白色页面2.5S~3S再出现启动页
分析解决:白屏为js文件解析阶段,将此时的白屏用启动页替换,到真正启动页停留时间完毕,跳转到主页。冷启动时间比热启动时间长,所以白屏时间也相对较长。
参考:React Native Android启动屏,启动白屏,闪现白屏
也可以直接使用第三方库react-native-splash-screen,原理一样

问题9

问题描述:文字过长添加省略号,最大长度阈值需要区分中英文,但中文和英文可容纳字符个数不同,在中英文混合的情况下如何动态控制可显示文字的长度
分析解决:在指定长度的区域内显示文字,超过可容纳长度用省略号表示,中文和英文可容纳字符个数不同,一个中文占两个英文长度,以中文文字可显示标准为例,为了更好的显示效果,就需要动态转换可显示中文文字个数,例如“你hhh好kk啊”转换为中文标准长度就是5.5个,提供可显示中文长度的最大值,截取可显示文字即可。

static CutStr(str, len){
    if (str.replace(/[^\x00-\xff]/g, "**").length <= 2 * len) {
        return str;
    }
    let char_length = 0;
    for (let i = 0; i < str.length; i++){
        let son_str = str.charAt(i);
        encodeURI(son_str).length > 2 ? char_length += 1 : char_length += 0.5;
        if (char_length >= len){
            let sub_len = char_length == len ? i+1 : i;
            return str.substr(0, sub_len) + '...';
            break;
        }
    }
}

问题10

问题描述:网络稳定的情况下,不能确保mqtt消息的正常接收,mqtt消息心跳检测,重连机制
分析解决:在物联网应用里,消息传输通常使用mqtt协议,MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。确保消息的正常接收就需要保证客户端和服务器的连接,通过重连机制和心跳检测来确保连接的可靠性。

重连机制:客户端和服务器mqtt建立连接失败/断开连接了则开启重连模式;三秒后进行一次重连,重连时首先检查网络是否正常,其次判断mqtt连接是否已经建立,如果没有建立连接才进行连接请求;当连接建立成功时,关闭重连循环,并开始消息订阅。
心跳检测:在RN的Android系统下,后台任务可以通过HeadlessJS 来实现,react-native-background-job就是基于HeadlessJS 实现的用来处理后台程序的第三方库,这里使用第三方库react-native-background-job做辅助,在应用切换到后台时,通过循环处理后台任务来确保连接不被断开,当应用再次回到前台时取消后台任务的执行。

//注册后台任务
BackgroundJob.register({
    jobKey: GlobalConstant.REGULAR_JOB_KEY,
    job: () => console.log("Running in background")
});

//前后台切换
handleAppStateChange(appState) {
    console.log('当前状态为:' + appState);
    if (appState === 'active') {
        //回到前台
        BackgroundJob.cancel({jobKey: GlobalConstant.REGULAR_JOB_KEY});
    }else if(appState == 'background'){
        //后台运行,触发执行
        BackgroundJob.schedule({
            jobKey: GlobalConstant.REGULAR_JOB_KEY,
            period: 2000,
            exact: true
        });
    }
}

问题11

问题描述:Android9.0网络请求都是失败
分析解决: Android9 开始,也会默认阻止 http 请求,督促开发者使用https请求。

方法一:APP网络请求更改为HTTPS(推荐);
方法二:targetSdkVersion 降到27及以下;
方法三:在 res 下新增一个 xml 目录,然后创建一个名为:network_security_config.xml 文件(名字自己取) ,内容如下,大概意思就是允许开启http请求

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

在项目的AndroidManifest.xml文件下的application标签增加以下属性,应用以上配置。

<application
     ...
    android:networkSecurityConfig="@xml/network_security_config"
    ...>
</application>

问题12

问题描述:github下载代码报错

fatal: unable to access 'https://github.com/youyanping/react-native-project.git/': Failed to connect to 127.0.0.1 port 1080: Connection refused

分析解决

git config --global http.proxy 查询到当前设置了代理,所以我取消这个设置,
git config --global --unset http.proxy 再查询,已经没有了代理,然后再clone,成功了!

问题13

问题描述:四指触控的报错

enabled a touch event which was not counted in 'tracedeTouchCount'

分析解决:网上人说是ReactNative 框架本身的bug,可以通过修改RN的源码解决,之后没改源码,打包成正式apk就不会有这个问题了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容