如何修改 React Native 的默认字体
作者:张博轩
title:脉脉高级前端工程师
原文链接:https://juejin.cn/post/6918447887940583438
引子
时间过得好快,不知不觉,加入脉脉已经快一个月了。之前的工作中主要做一些 PC、H5、Node 相关的开发,开发RN还是头一次接触,感觉还挺好玩的。😂
为什么要设置默认字体?
前两天Fix了一个RN端的BUG,同事的小米10Pro手机上发现文字被遮挡。如下图所示:
不仅是小米,一些 Android 的其他机型也会遇到类似的问题。因为 Android 手机厂商很多很多,不像 iPhone 只有一家公司,默认字体是不统一的。这时候如果组件没有设置字体,就会使用手机的默认字体。而有些字体,比如 “OnePlus Slate”、“小米兰亭pro” 在使用 Text 组件渲染的时候,就会出现被遮挡的问题。
那么,如何解决这个问题呢?
如何实现全局字体的修改
自定义组件
第一种思路比较简单,可以封装 Text 组件,针对 Android 系统设置默认字体。
首先,创建一个新文件,命名为:CustomText.js。
// CustomText.js
import React from "react";
import { StyleSheet, Text, Platform } from "react-native";
// Fix Android 机型文字被遮挡的问题
const defaultAndroidStyles = StyleSheet.create({
text: {
fontFamily: ""
}
});
// 这里针对 web 加一个简单的样式,方便测试
const defaultWebStyles = StyleSheet.create({
text: {
color: "#165EE9"
}
});
const CustomText = (props) => {
let customProps = {
...props
};
if (Platform.OS === "android") {
customProps.style = [defaultAndroidStyles.text, props.style];
}
if (Platform.OS === "web") {
customProps.style = [defaultWebStyles.text, props.style];
}
delete customProps.children;
const kids = props.children || props.children === 0 ? props.children : null;
return <Text {...customProps}>{kids}Text>;
};
export default CustomText;
接下来,在 App.js 中使用这个组件。
// App.js
import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { CustomText } from './CustomText';
const App = () => {
return (
<View style={styles.container}>
<Text style={styles.text}>使用Props传递style样式Text>
<CustomText>使用CustomText自带style样式CustomText>
View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#070825'
},
text: {
color: '#ff0000'
}
});
如下图所示,
但是这种处理方式需要将所有引入、使用组件的都改为
覆盖Text组件的render方法
首先,增加一个函数,来覆盖 Text 的 render 方法:
// util.js
import { Text, Platform } from "react-native";
export const setCustomText = () => {
const TextRender = Text.render;
let customStyle = {};
// 重点,Fix Android 样式问题
if (Platform.OS === "android") {
customStyle = {
fontFamily: ""
};
}
// 为了方便演示,增加绿色字体
if (Platform.OS === "web") {
customStyle = {
lineHeight: "1.5em",
fontSize: "1.125rem",
marginVertical: "1em",
textAlign: "center",
color: "#00ca20"
};
}
Text.render = function render(props) {
let oldProps = props;
props = { ...props, style: [customStyle, props.style] };
try {
return TextRender.apply(this, arguments);
} finally {
props = oldProps;
}
};
};
这里参考了Ajackster/react-native-global-props 中 setCustomText 的实现。然后在 App.js 中,调用 setCustomText 函数即可。
// App.js
import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { CustomText } from './CustomText';
import { setCustomText } from "./util";
setCustomText();
const App = () => {
return (
<View style={styles.container}>
<Text>通过调用 utils.setCustomText() 修改Text>
<Text style={styles.text}>使用Props传递style样式Text>
<CustomText>使用CustomText自带style样式CustomText>
View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#070825'
},
text: {
color: '#ff0000'
}
});
如下图所示,我们新增了一个
仅仅需要执行一遍这个函数,就可以影响到所有
demo地址:https://codesandbox.io/s/wizardly-kirch-y3ddr
原理浅析
React Native Text 组件
Text 组件是一个类组件,在它的 render 方法中,首先解构了 props 属性,然后根据是否存在祖先节点,以及内部的状态合并 props 属性。
render 方法经过 babel 的编译之后,会转换成 React.createElement 所包裹的语法树,从而转换成虚拟的Dom树。这就可以联想到另一个API :
React.cloneElement(
element,
[props],
[...children]
)
我们在覆盖 Text.render 方法时,只需要使用 Text.prototype.render.call 得到之前的节点,更新 props ,并调用 React.cloneElement 得到一个新的节点返回即可。
import React from 'react';
import { StyleSheet, Text } from 'react-native';
const styles = StyleSheet.create({
defaultFontFamily: {
fontFamily: 'lucida grande',
},
});
export default function fixOppoTextCutOff() {
const oldRender = Text.prototype.render;
Text.prototype.render = function render(...args) {
const origin = oldRender.call(this, ...args);
return React.cloneElement(origin, {
style: [styles.defaultFontFamily, origin.props.style],
});
};
}
搜索官方 issue,会找到类似的问题:https://github.com/facebook/react-native/issues/15114,就是用的这种解决思路。
react-native-global-props 的实现
Ajackster/react-native-global-props 是一个可以添加默认组件属性的库。下面摘自 setCustomText.js
import { Text } from 'react-native'
export const setCustomText = customProps => {
const TextRender = Text.render
const initialDefaultProps = Text.defaultProps
Text.defaultProps = {
...initialDefaultProps,
...customProps
}
Text.render = function render(props) {
let oldProps = props
props = { ...props, style: [customProps.style, props.style] }
try {
return TextRender.apply(this, arguments)
} finally {
props = oldProps
}
}
}
它覆盖了 Text 组件的静态属性:defaultProps 和 render 方法。这里不一样的是,它没有借助 React.cloneElement 返回一个新的节点,而是在返回结果的前后,修改 props 中的 style属性,这里等同于修改 arguments[0] 的值,因为他们的引用相同。并在最后重置props,避免 props 被污染。可以看出,这种方式实现的更加巧妙。
styled-components css.Text 为什么会受影响
styled-components
是一个React
的第三方库,是CSS in JS
的优秀实践。它对于 React Native 也有着不错的支持。因为 React Native 修改样式只能通过修改 style 属性来完成,所以 CSS in JS
的方案对于 React Native 项目来说有着天然的优势。
对于 React 项目,styled-components
会修改className 属性来达到修改样式的目的;而对于React Native,则是使用下面的方法,修改组件props中的style属性来达到目的。
propsForElement.style = [generatedStyles].concat(props.style || []);
propsForElement.ref = refToForward;
return createElement(elementToBeCreated, propsForElement);
但是,这个修改的过程一定是在我们重写 render 函数之前完成的。所以,上面那个方法修改style对于styled-components
创建的React Native组件同样适用。
结语
关于如何修改React Native的全局样式的讨论暂时告一段落了。第一次发现还可以以这样的方式修改 React 的 render 函数的属性,感觉还是比较神奇的。也是第一次尝试写这种偏原理探究的文章,如果有写的不对的地方,或者想和我交流的话,欢迎评论留言哈~
PS:对脉脉感兴趣的小伙伴,欢迎发送简历到 496691544@qq.com ,我可以帮忙内推~
参考
React-Native Text文档 https://reactnative.cn/docs/text Two-way to change default font family in React Native https://ospfolio.com/two-way-to-change-default-font-family-in-react-native/ 一文解决RN0.58部分安卓手机text显示不全问题 https://segmentfault.com/a/1190000023622085 react-native-global-props https://github.com/Ajackster/react-native-global-props styled-components 源码阅读:https://github.com/wangpin34/blog/issues/49 ReactNative源码篇:源码初识:https://github.com/sucese/react-native/blob/master/doc/ReactNative%E6%BA%90%E7%A0%81%E7%AF%87/1ReactNative%E6%BA%90%E7%A0%81%E7%AF%87%EF%BC%9A%E6%BA%90%E7%A0%81%E5%88%9D%E8%AF%86.md