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 来说则有两种解决方案:
将原生 Text 替换成自定义 Text 组件(官方推荐)
The recommended way to use consistent fonts and sizes across your application is to create a component
MyAppTextthat includes them and use this component across your appimport 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> ); }参考 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>
);
}
react-native-scrollable-tab-view
react-native-scrollable-tab-view 组件作为 ScrollView 的子组件时不可用,会出现 tab 选中不一致,tab content 无法撑开的现象,详情看:https://github.com/ptomasroos/react-native-scrollable-tab-view/issues/982
设置环境变量
通常我们的 web 项目可以在 npm script 里通过设置 NODE_ENV 来设置不同的环境变量。比如NODE_ENV=develope node index.js, 之后可以在代码中通过process.env.NODE_ENV来判断具体是哪个环境。rn 项目要达到同样的目的,则需要额外安装 babel 插件 babel-plugin-transform-inline-environment-variables(或者可以通过 react-native-config,不过这个方法没有去验证,因为低于 6.x 版本的需要客户端 link)
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 依然要注意性能优化。比如列表项中如果有图片,那么要注意压缩大小等。
未完待续~