前端学习踩坑(4)—— 自定义组件

引言

由于一些样式和功能的特殊要求,RN自带的很多组件已经无法满足我的开发要求了,所以我开始了项目RN组件库的初期搭建。

听起来很高大上,但是其实目前我的办法就是在项目里面新建一个组件,然后用接口约束props,在需求和现有组件的基础上自己封装一些组件出来,下面我就挑几个说说。

WCTextInput

首先,我将组件实现功能所需要的props类型抽象为接口来进行约束:

image-20220816193645532

由于设计的需求,实际上输入框分为三种类型:

image-20220816193758262

输入框前面有提示的

image-20220816193745093

密码类型的(多了是否可见的按钮 + 文本不可显示)

image-20220816193856915

最普通的

一开始,我的想法是先使用最基本的View、TextInput、Text为基础封装一个最普通的输入框,并在普通的输入框上继续封装上述的两种较为复杂的输入框。但是在我和亲爱的mentor沟通过后,他给了我一个很好的思路。

其实没有必要封装那么多,是否有前面的提示完全可以自适应渲染。只有判断content不为空字符串的时候才会渲染,否则就直接不渲染那个Text。密码和普通的文字输入,也可以在props中添加一个type来修饰,这样大大减少了组件之间封装和传递的多余代码,非常的Amazing!

代码讲解

1
2
3
4
5
6
7
8
9
10
11
12
content?: string,
placeholder?: string,
keyboardType?: KeyboardTypeOptions,
focusedViewStyle?: StyleProp<ViewStyle>,
unfocusedViewStyle?: StyleProp<ViewStyle>,
width?: number,
height?: number,

onChangeText(text: string): any,

value?: string,
type?: WCTextInputType
  • content(可选参数)

提示信息,渲染在输入框的前方。如果为空字符串或不设置,则将输入框渲染为整个组件的完全大小。

  • placeholder(可选参数)

输入框中的提示信息

  • keyboardType(可选参数)

键盘类型,默认值为default,可选值有:image-20220816194907532

  • focusedViewStyle(可选参数)

设置被聚焦到时的样式,默认样式为设计规范中所规定的

  • unfocusedViewStyle(可选参数)

设置不被聚焦到时的样式,默认样式为设计规范中所规定的

  • width(可选参数)

设置宽度,默认值为295px

  • height(可选参数)

设置高度,默认值为56px

  • onChangeText(必要参数)

文本改变时的回调函数

  • value(可选参数)

输入框中的初始值

  • type(可选参数)

输入框的类型,包括

image-20220816195311680

text为普通文本输入框,password为密码输入框,自带一个用于控制密码可见性的按钮以及文本的可见性功能。

优雅的初始值赋值

1
2
3
4
let {
content, placeholder, keyboardType = 'default', width = 295, height = 56, value, onChangeText, focusedViewStyle,
unfocusedViewStyle, type = WCTextInputType.text
} = props;

在使用解构赋值的基础上,我从mentor那里学到了一种优雅的初始值赋值方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const changeDivStyle = () => {
// 表示至少传了一个自定义样式,则应该使用自定义样式
if (focusedViewStyle || unfocusedViewStyle) {
if (ifFocus) {
return focusedViewStyle;
} else {
return unfocusedViewStyle;
}
} else {
if (ifFocus) {
return Styles.inputDivFocused;
} else {
return Styles.inputDivUnfocused;
}
}
}

样式控制函数,如果传入了一个自定义样式,则应该使用自定义样式,并且返回的样式应该由是否聚焦来控制。

最后根据type进行条件渲染,返回组件即可。

效果展示

  • 选中状态下

image-20220816195753274

  • 非选中状态下

image-20220816195823976

  • 密码可见状态下

image-20220816195858481

WCNavigation

代码讲解

1
2
3
header?: string;
navigation: any
backName: string;
  • header(可选参数)

显示在导航栏上的标题

  • navigation(必要参数)

用于导航的navigation参数

  • backName(必要参数)

单击左上角返回按钮要返回到的Navigation名称

WCButton

代码讲解

1
2
3
4
5
6
7
text: string,
usableViewStyle?: StyleProp<ViewStyle>,
unusableViewStyle?: StyleProp<ViewStyle>,
width?: number,
height?: number,
onPress(): any,
usable: boolean,
  • text(必选参数)

按钮内的文字

  • usableViewStyle(可选参数)

按钮可使用时的样式

  • unusableViewStyle(可选参数)

按钮不可使用时的样式

  • width(可选参数)

按钮宽度,默认值为295px

  • height(可选参数)

按钮高度,默认值为64px

  • onPress(必选参数)

触发后的回调函数

  • usable(按钮可用性)

按钮是否可用,默认值为true(不可用状态下按钮无点击动画且无法触发回调函数)

WC验证码输入组件

这个组件暂时未封装,感觉似乎没有什么复用的价值(毕竟好像只用在登录短信验证码输入),这里只记录一下编写过程

其中有两个核心的难点:

  • 跨状态保存短信验证码发送冷却时间

短信验证码冷却发送时间不仅仅要在当前页面内保存,否则只需要退出该页面再返回就可以直接重置验证码cd。

  • 特殊的布局要求

image-20220817110935810

由于当前项目正好使用redux来进行了状态管理,所以我们可以将冷却时间和是否冷却作为参数存入redux的loginSlice中管理。并在当前组建中新建两个ref,一个用来保存是否冷却,一个用来保存冷却时间。(是否冷却其实可以由冷却时间推出,但是为了方便后面的条件渲染,所以增加了这么一个变量)。

1
2
3
4
5
6
7
8
9
10
let login = useSelector(selectLogin);
const dispatch = useDispatch();

const reloadTimeSaver = useRef(login.reloadTime);

const ifReloadSaver = useRef(login.ifReload);

const [reloadTime, setReloadTimeFunc] = useState(reloadTimeSaver.current);

const [ifReload, setIfReloadFunc] = useState(ifReloadSaver.current);

使用ref是为了防止在setInterval中由于闭包导致state的无法正常更新。

然后,我们分析能让验证码进入cd的两个条件:

  • 进入页面后redux中仍然保存还在冷却中(上一次发送完还未冷却成功)
  • 进入页面后冷却时间结束,点击重新发送

为了解决第一个套件,这里我们使用无依赖参数的useEffect来使得组件被挂载时就根据redux中的数据判断是否需要渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
useEffect(() => change(), []);


const setReducer = () => {
const timeReducer = setInterval(() => {
console.log(ifReloadSaver.current);
reloadTimeSaver.current--;
dispatch(setReloadTime({reloadTime: reloadTimeSaver.current}));
if (reloadTimeSaver.current < 0) {
ifReloadSaver.current = false;
dispatch(setIfReload({ifReload: ifReloadSaver.current}));
reloadTimeSaver.current = RELOAD_TIME;
dispatch(setReloadTime({reloadTime: reloadTimeSaver.current}));
clearInterval(timeReducer);
}
}, 1000);
}

const change = () => {
if (ifReloadSaver.current) {
setReducer();
postVerificationCode({
phone: login.phone,
captcha: '',
});
}
}

为了解决第二个条件,我们为“重新发送”按钮编写新的回调函数:

1
2
3
4
5
6
7
8
9
10
const reloadBtn = () => {
if (!ifReloadSaver.current) {
console.log('cfl');
ifReloadSaver.current = true;
reloadTimeSaver.current = RELOAD_TIME;
dispatch(setReloadTime({reloadTime: reloadTimeSaver.current}));
dispatch(setIfReload({ifReload: ifReloadSaver.current}));
setReducer();
}
}

然后在return中根据ifReload进行条件渲染即可。

为了解决特殊的样式布局需求,最开始我有两种思路:

  • 6个框每个都是一个TextInput,使用代码控制他们的焦点移动(即输完一个之后自动跳下一个)
  • 利用一个绝对定位的TextInput盖在6个View上面,将TextInput的背景色、文字色,选中色设为透明并将contentWindow设为false,然后利用这个不可见的TextInput中输入的数据来渲染6个View

第一种听起来就很复杂,第二种实际上RN官方给出的原生Api就足够我们实现功能,所以果断第二种。

布局在此不做解释,只是一个简单的absolute。

返回的组件如下:

1
2
3
4
5
6
7
8
<TextInput style={Styles.codeInput} selectionColor={'rgba(255,255,255,0)'} contextMenuHidden={true}
keyboardType={'number-pad'} onChangeText={(text) => {
let newText = text.replace(/\D/, '');
if (newText.length > 6) {
newText = String(newText.charCodeAt(6) - 48);
}
setCode(newText);
}} value={code}></TextInput>

在style中,我们将background-color和color设置为了透明。对于selectionColor,我们使用rgba(255,255,255,0)来设置透明。直接使用transparent来设置在安卓上是完美支持的,但是在ios端选中后仍有灰色的选中背景色,即使使用rgba(0,0,0,0)也不行,查阅资料后发现只能使用rgba(255,255,255,0)。

contextMenuHidden是否隐藏选中文本后上面的选项(比如全选、复制等)。keyboardType表示键盘类型,由于是短信数字验证码,所以直接使用number-pad。

onChangeText中的回调函数主要一个只能输入数字的约束,同时还有如果输入长度超过6,会将value清空从头开始输入(会将第7个输入设为下一次输入的第1个),code使用state保存。

至此,验证码输入组件完成。·