RN 避坑指北

记录平时开发 rn 时遇到的问题,特指 0.59 版本,不同版本可能情况不同,仅供参考。

一像素问题

rn 解决一像素很简单,只需要 StyleSheet.hairlineWidth 即可解决。

TextInput 组件

自带内边距,多行文字时文字居中展示,想要消除需要设置样式如下:

// 文字向上对齐
textAlignVertical: 'top'
// 消除内边距
padding: 0

Placeholder 在 ios 和 android 下默认颜色表现不一致,可以通过 placeholderTextColor 属性进行设置:

placeholderTextColor="#CCCCCC"
阴影

因为设计理念的问题,android 下几乎无法实现和 ios 一致的阴影效果。而 flutter 基于 skia 引擎渲染,应该会有比较好的表现?有空尝试下。 🤔

border 虚线

border 虚线也是一个比较棘手的问题, android 上实测无效,详情看 issue。解决方案是循环一个 pattern 然后 overhidden 掉,又多了一个让我想尝试 flutter 的理由 😂

多行文字

多行文字在 android 机型下有可能会出现高度不够导致文字被截掉的问题,解决方法:设置 lineHeight

实现不同字号的文字底部对齐

例如 ¥500 想要 ¥ 字号小一点,而 500 字号大一点,但是需要底部对齐,则可以用 Text 包裹起来,如下:

<Text>
  <Text style={{ fontSize: 12 }}>¥</Text>
  <Text style={{ fontSize: 20 }}>500</Text>
</Text>
Text 组件样式继承

众所周知,css 如果想设置默认的全局样式相当简单,比如字体颜色默认为 ‘#323232’ , 只要在 body 上设置一下 color: #323232 即可 ,然而对于 RN 来说则有两种解决方案:

  1. 将原生 Text 替换成自定义 Text 组件(官方推荐)

    The recommended way to use consistent fonts and sizes across your application is to create a component MyAppText that includes them and use this component across your app

    import React from 'react';
    import { TextProps, StyleSheet, Text } from 'react-native';
    
    const styles = StyleSheet.create({
      defaultStyle: {
        color: '#323232',
      },
    });
    
    export default function CustomText(props: TextProps & { children?: any }) {
      const { style, ...restProps } = props;
      return (
        <Text style={[styles.defaultStyle, style]} {...restProps}>
          {props.children}
        </Text>
      );
    }
  2. 参考 Ajackster/react-native-global-props 的实现,在项目入口位置,修改组件的 render 方法。

方案二虽然方便,但不是官方解法,万一有坑就 GG 了。而方案一如果是已有项目,则需要批量修改代码。
这里顺便提一下在 vscode 里批量替换的方法:

// 将正则,大小写,全匹配 三个按钮点亮
find: Text(,|\s)(.*react-native';)
replace: $2\nimport Text from 'Your/Text';

一开始我是这么写的,只考虑到了单行,结果下面这种情况没有替换掉:

import {
  StyleSheet,
  ViewStyle,
  View,
  Text,
  Image,
  TouchableOpacity,
} from 'react-native';

后面换成 \sText[,|\s]\n?([\s\S]*'react-native';) 在浏览器里可以匹配上,但是 vscode 无法匹配 🤔。 最后简单写个脚本补全上面的漏网之鱼:

const fs = require('fs');
const util = require('util');
const path = require('path');
const glob = require('glob');

const replace = async () => {
  try {
    glob('src/**/*.tsx', {}, function (er, files) {
      const reg = /\sText[,|\s]\n?([\s\S]*'react-native';)/;
      files.forEach(async (file) => {
        const content = await util.promisify(fs.readFile)(file, 'utf8');
        const newContent = content.replace(reg, (m, p1) => {
          return ` ${p1.trim()}
import Text from '@/components/Text';`;
        });
        await util.promisify(fs.writeFile)(file, newContent, 'utf8');
      });
    });
  } catch (error) {
    console.log(error);
  }
};

replace();

🤔 上面 CustomText 的写法有一个问题,即嵌套 Text 的样式如果是自定义组件中默认的样式,将不会被继承:

<Text style={{ color: 'red' }}>
  <Text>red</Text>
  <Text>red</Text>
</Text>

<CustomText style={{ color: 'red' }}>
  <CustomText>not red</Text>
  <CustomText>not red</Text>
</CustomText>

改良版:

import React, { Children, isValidElement, cloneElement } from 'react';
import { TextProps, StyleSheet, Text } from 'react-native';

const styles = StyleSheet.create({
  defaultStyle: {
    color: '#323232',
  },
});

export default function CustomText(
  props: TextProps & { children?: any; useDefaultStyle?: boolean },
) {
  const { style, useDefaultStyle = true, ...restProps } = props;
  const newStyle = useDefaultStyle ? [styles.defaultStyle, style] : style;

  const childrenWithProps = Children.map(props.children, (child) => { 
   if (isValidElement(child)) {
     // 如果子元素是个组件,则子组件不使用默认 style
      return cloneElement(child as React.ReactElement<any>, {
        useDefaultStyle: false,
      });
    }
    return child;
  });

  return (
    <Text style={newStyle} {...restProps}>
      {childrenWithProps}
    </Text>
  );
}
ScrollView 与 Keyboard

场景: 当 ScrollView / FlatList / SectionList 中包含输入框时,点击输入框唤起键盘。如果此时输入框右侧有清除输入的按钮,点击时首先会将键盘收起,之后再次点击才会执行 onPress。

方案:将 keyboardShouldPersistTaps 属性设置为 always,点击清除输入的按钮,键盘不会自动收起。

iOS Text

ios 的 Text 组件 borderRadius | textAlignVertical 属性无效,只能外层再包个 View

Border

RN 的盒模型是 border-box, 即 宽高包括了 padding 和 border。

在开发中发现如果容器具有 backgroundColor 与 borderRadius 后,设置一个不同颜色的 border, 该容器外围会有一层浅浅的 backgroundColor 描边(目前仅 ios 有这种情况)解决方案:在容器外层再包一个 View,设置 border 样式

FlatList 的 onViewableItemsChanged

onViewableItemsChanged 的方法在 FlatList 的生命周期中不可以改变,否则报错:

Invariant Violation: Changing onViewableItemsChanged on the fly is not supported

如果是类组件,可以在 constructor 中定义该方法,如果是函数组件,可以通过 useRef 包裹该方法。

等宽字体

如果字体不是等宽的话,倒计时组件会产生宽度抖动。ios 和 安卓平台的字体并不通用,参考 https://github.com/react-native-training/react-native-fonts。 想要设置等宽字体可以设置 Text 如下样式属性:

fontVariant: ['tabular-nums'] // 仅 IOS
fontFamily: Platform.OS === 'android' ? 'monospace' : undefined // 如果是安卓则设置字体为 monospace

注意如果 fontFamily 指定的字体不存在,则页面将报错

轮播图

轮播图一开始用的是社区里 star 数比较高的 https://github.com/leecade/react-native-swiper/issues/932

☠️ 但是这个组件存在一个坑:如果 loop 为 true,rerender 的时候,会先展示最后一项,再闪回到第一项。这个 bug 存在好几年了,提了很多 issue,都没有解决掉,例如 https://github.com/leecade/react-native-swiper/issues/932

由于项目比较紧张,又没有时间自己去实现一个,后面发现 https://github.com/f111fei/react-native-banner-carousel 这个仓库不存在该问题。后续还是需要自己去研究一下 rn 的轮播图实现方案。

stickyHeaderIndices

ScrollView 提供 stickyHeaderIndices 可以指定滚动时吸顶的组件索引

☠️ 这个属性如果是动态的会导致 ScrollView 无法正常渲染,见 // https://github.com/facebook/react-native/issues/25157 ,解决的方法是将 removeClippedSubviews 设置成 false

关闭弹窗与跳转新页面

在我们的 ios 客户端发现一个问题:点击一个 Modal 中的内容后,关闭该 modal 并打开一个新的 webview,此时有很大概率无法打开新 webview。

猜测是由于关闭 modal 的动画影响到打开新 webview(不清楚是否 ios 原生就是如此)

解决方案就是延迟 300ms 后再打开新 webview

SectionList scrollToLocation

在 stickySectionHeadersEnabled 为 true 的时候,以下代码的作用是滚动到第一个 section 的第一个元素。但是在 ios 和 android 平台表现不一致,ios 会认为 sticky 元素需要占位置,而安卓则不占位,导致安卓下列表会被遮挡。

sectionListRef.current.scrollToLocation({
  sectionIndex: 0,
  itemIndex: 0,
});

解决方案:根据平台添加 viewOffset 属性,设置滚动偏移量。

FlatList 性能优化

RN 的长列表性能一向不太好,即使用了 FlatList 依然要注意性能优化。比如列表项中如果有图片,那么要注意压缩大小等。

未完待续~