RN-起步

总是应该认识一些基本概念

原生组件

在 rn 中不可以使用 html 标签,只能使用 rn 提供的原生组件,或第三方提供的可以用于 rn 的组件

原生组件列表请参看官方链接:https://reactnative.cn/docs/components-and-apis

主要有以下核心组件:

组件名 等效 html 标签 重要的特殊之处
View div 无点击事件
Text span 所有直接文本必须包裹在 <Text> 之中,包括实体字符
Image img 必须指明高度和宽度
ScrollView - ScrollView 不可以嵌套在另一个 ScrollView 中
TextInput input -

样式

有了结构,自然要有样式

内联的样式可以直接通过组件的 style 属性进行书写,每个属性使用驼峰写法

注意每个标签可以具有的 style 是不一样的,比如 color 属性,无法设置在 View 组件上(因为 View 不能直接包含文本),只能设置在 Text 组件上

也可以使用内部样式,写法如下

1
2
3
4
5
6
7
8
9
10
11
import {StyleSheet} from 'react-native';

const styles = StyleSheet.create({
container: {
minHeight: '100%',
backgroundColor: 'white',
},
placeholder: {
height: 60,
},
});

create 之后的 styles 实质上也还是一个对象,可以通过 styles.container 来使用样式,只不过通过 StyleSheet.create 来创建的话,会有自动代码补全

使用时,直接书写于组件的 style 属性中。注意,react 原本是写于 className 属性中,此处不同

1
2
3
<View style={styles.container}>
<Text>demo</Text>
</View>

也可以使用 spread 运算符

1
2
3
<View style={{...styles.container, backgroundColor: 'black'}}>
<Text>demo</Text>
</View>

在上一章中我们引入了 less 支持,所以当然也可以写外部样式

1
2
3
4
5
// index.less
.container {
min-height: 100%;
background-color: white;
}
1
import styles from './index.less'

之后用法与内部样式一样

基本布局

为了达到类似 caho 的移动端效果,我们需要页脚

则有如下组件

1
2
3
4
5
6
7
8
9
10
11
12
import {SafeAreaView, ScrollView, View} from 'react-native';
const Layout = () => {
return (
<SafeAreaView style={styles.container}>
<ScrollView>
<Component navigation={navigation} />
<View style={styles.placeholder} />
</ScrollView>
<Footer navigation={navigation} />
</SafeAreaView>
);
}

首先使用 SafeAreaView 作为外层,以做到 ios 支持。详见 https://reactnative.cn/docs/safeareaview

然后使用 ScrollView 包裹主要视图,使得主视图组件可以上下滑动;同时,提供一个自定义的 Footer 组件作为页脚

注意,此处页脚必须写在 ScrollView 外部,才可以固定在视图下部,起到类似于 positon: fixed; 之效果

为什么要这样?因为 rn 的 style 不支持 position: fixed;

如果写在 ScrollView 内部,就会造成必须滚动到页面最下方才可以看到页脚

同时,因为页脚现在类似 fixed,所以会遮挡掉 ScrollView 最末尾的部分,所以需要在 ScrollView 中放置一个等高的空组件来占位

路由

路由如何配置,是非常重要的一环,必须要了解

设计

参考 Caho,我暂且设置了如下路由

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
interface Route {
path: string;
title: string;
component: React.ComponentType<any>;
}

const router: Route[] = [
{
path: '/login',
title: '登录',
component: LoginScreen,
},
{
path: '/forum',
title: '首页',
component: HomeScreen,
},
{
path: '/forum/[id]',
title: '版块',
component: ForumIdScreen,
},
{
path: '/forum/all',
title: '所有版块',
component: ForumScreen,
},
{
path: '/topic/[id]',
title: '帖子',
component: TopicScreen,
},
{
path: '/user/[id]',
title: '用户',
component: UserScreen,
},
{
path: '/message',
title: '消息',
component: MessageScreen,
},
];

export default router;

配置

使用 react-navigation 来实现路由

文档地址:https://reactnavigation.org/docs/getting-started/

安装依赖

1
yarn add @react-navigation/native @react-navigation/native-stack react-native-screens react-native-safe-area-context

然后打开 App.tsx,引入需要的组件

1
2
3
4
import {NavigationContainer} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

react-navigation 要求我们通过 NavigationContainerStack 注册路由,之后才可以正常使用

注册写法如下

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
28
29
30
31
32
33
34
35
36
import router from './router';

const createScreen =
(Component: React.ComponentType<any>) =>
({navigation}: {navigation: Navigation}) => {
return (
<SafeAreaView style={styles.container}>
<ScrollView>
<Component navigation={navigation} />
<View style={styles.placeholder} />
</ScrollView>
<Footer navigation={navigation} />
</SafeAreaView>
);
};

const Router = () => {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="/forum">
{router.map(route => (
<Stack.Screen
key={route.path}
name={route.path}
component={createScreen(route.component)}
options={{
title: route.title,
headerTitleAlign: 'center',
headerRight: () => <Logout />,
}}
/>
))}
</Stack.Navigator>
</NavigationContainer>
);
}

首先使用 <NavigationContainer> 创建 navigation 容器,然后使用 <Stack.Navigator> 创建路由栈,用于在不同路由之间往返,最后通过 <Stack.Screen> 声明每一个路由

对于每条路由,可以手写,也可以如上这般通过数组遍历来完成,但要注意,<Stack.Navigator> 只接受 <Stack.Screen> 作为直接子级

对于每个 Screen,必要参数有 namecomponent,用于指示其唯一标识符和要渲染的组件。上例中的 key 是因为数组方法 map 要求对每个组件传入 key 作为唯一标识符

此处的 component 使用了一个包装函数,来使得所有组件共用相同的布局,不需要各自引入页脚

此外,还可以传入 options 来指示路由的标题等内容。上例中传入了 title 作为标题,headerTitleAlign 作为标题的对齐方式,headerRight 作为附加在标题右侧的组件。该标题会渲染在每个页面的最上方,等效顶边栏

现在就完成路由的配置了,启动应用就会发现应用路由到了 initialRouteName 所指示的组件

如果没有指示 initialRouteName,则会路由到 <Stack.Screen> 列表中的第一个组件

跳转

可是这样只能到达一个页面呀,该怎么前往其它页面呢

可以使用注入的 navigation

对于通过路由到达的每个组件,其 props 中均会被注入一个 navigation 导航器,该导航器具有两个重要方法:

  1. navigate: (navigationName: NavigationName, payload?: any) => void;,第一个参数是路由名 (name),第二个参数可选,是通过路由传递的参数
  2. getState: () => any;,可以获得上层路由传递过来的参数,包含路由栈内全部的参数

显然,这是归属于路由的功能,但是 react-navigation 不提供 navigation 的类型定义,所以我们可以在 ./src/router/index.ts 中提供我们自己的类型定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const paths = [
'/login',
'/forum',
'/forum/[id]',
'/forum/all',
'/topic/[id]',
'/user/[id]',
'/message',
] as const;
type NavigationName = typeof paths[number];
export interface Navigation {
navigate: (navigationName: NavigationName, payload?: any) => void;
getState: () => any;
}

通过这样的定义,我们就可以在外部使用 navigation 的使用,声明其类型为 Navigation,并在其 navigate 方法中使用代码自动补全

然后,我们可以在任意的直接路由到达的组件中,获取 navigation 导航器,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ./src/screen/user/index.tsx
import React from 'react';
import {Text, View} from 'react-native';
import {Navigation} from '../../router';

export default (props: {navigation: Navigation}) => {
const {navigation} = props;
const {routes} = navigation.getState();
const {id} = routes[routes.length - 1].params;

return (
<View>
<Text>{id}</Text>
</View>
);
};

则从页脚跳转到 /user/[id] 的代码如下

1
navigation.navigate('/user/[id]', {id: userId})

页脚

刚才一直说页脚,它是怎么实现的呢?

其实就是在 View 里面放 Icon,2333

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React from 'react';
import {View} from 'react-native';
import {connect} from 'react-redux';

import Icon from '../../iconfont';
import {User} from '../../interface';
import {Navigation} from '../../router';

import styles from './index.less';

const Footer = connect(
state => state,
dispatch => ({dispatch}),
)((props: any) => {
const {navigation, user}: {navigation: Navigation; user: User} = props;
return (
<View style={styles.footer}>
<Icon
name="home"
style={styles.footerIcon}
onPress={() => navigation.navigate('/forum')}
/>
<Icon
name="ring"
style={styles.footerIcon}
onPress={() => navigation.navigate('/message')}
/>
<Icon
name="person"
style={styles.footerIcon}
onPress={() => {
if (user) {
navigation.navigate('/user/[id]', {id: user.id});
} else {
navigation.navigate('/login');
}
}}
/>
</View>
);
});

export default Footer;

注意,此处使用了 onPress 属性,可以认为等效于在写 html 时的 onClick 属性,但是 View 上是没有该属性的,需要使用 TouchableOpacity 等可点击组件,但这是后面的内容了


感谢阅读

--It's the end.Thanks for your read.--