作为 react 开发者,学习 react native 你需要知道什么(上)

345 阅读8分钟

文档传送门:

RN:reactnative.dev/docs/enviro…

Expo:docs.expo.dev/tutorial/in…

概述

RN 由两块内容组成:一些特别的 React 组件,这些组件会被编译成原生 Widgets、另一块是原生平台的 APIs。注意只有 UI 会编译成原生代码,逻辑不会被编译。我们写的 Javascript 代码之后会跑在 RN App 的一个 Javascript 线程中,我们的 Javascript 代码通过 Javascript 虚拟机,虚拟机再通过 Bridge 与原生模块交互。

RN 的理念是 "Learn once, Write everywhere",IOS 与 Android 下实现同一样式可能使用的是不同的方法(比如容器阴影的效果),这些差异需要开发人员来手动敲代码各自实现,RN 并不会自动实现。

组件

基础组件

RN 和 React Web 组件的实现方式有所不同。RN 组件是针对移动应用开发的,它们是原生的 UI 组件,具有不同的实现方式。而 React Web 组件是用 HTML、CSS 和 JavaScript 构建的,是 Web 平台上的组件。常用的基础组件如下:

// 容器组件,相当于 <div></div>,
// 但是对于内容超出容器高度的情况是不能滚动的
// 默认使用 flexbox 布局
<View />

// 可滚动的容器组件,
// 一次会渲染所有节点,适合数据不多的滚动
<ScrollView />

// 可滚动的容器组件,
// 懒加载结点,适合长列表
<FlatList />

// 文本组件,负责展示文本。
// 注意在 RN 中,文本必须被 Text 组件包裹
<Text>文本</Text>

// 按钮,按钮的文本通过 title 属性赋值。
// 此外,RN 中点击事件是 onPress 而不是 onClick。
<Button
  onPress={onPressHandler}
  title="按钮"
/>

// 输入框
<TextInput
  onChangeText={onChangeText}
  value={text}
/>

// 图片,本地图片使用 require 引入,网络图片通过 uri 属性引入
<Image source={require('./img/check.png')} />
<Image
  source={{uri: 'https://reactjs.org/logo-og.png'}}
  style={{width: 400, height: 400}
/>

交互组件

和 Web 开发中不同的是,RN 中并不是每个组件都有点击事件,像 Image 和 View 都是没有 onPress 事件的,为了帮这些组件绑定点击事件,需要在包裹在特定的组件中来实现:

// 透明效果
<TouchableOpacity
  onPress={onPress}
  <Text>文本</Text>
</TouchableOpacity>

// 高亮
<TouchableHighlight onPress={onPress}>
  <View>
    <Text>文本</Text>
  </View>
</TouchableHighlight>

// 原生的反馈
<TouchableNativeFeedback
  onPress={() => {
    setRippleColor(randomHexColor());
    setRippleOverflow(!rippleOverflow);
  }}
  background={TouchableNativeFeedback.Ripple(rippleColor, rippleOverflow)}
>
  <View>
    <Text>文本</Text>
  </View>
</TouchableNativeFeedback>

// 无反馈
<TouchableWithoutFeedback onPress={() => alert('Pressed!')}>
  <MyComponent />
</TouchableWithoutFeedback>;

// 这几个组件的区别就是在点击后的反馈效果不同

样式

与web开发的区别

  1. 样式属性命名:RN 中的样式属性命名和 CSS 中的命名有所不同,比如 background-color 在 RN 中写作 backgroundColor
  2. 像素单位:RN 中的样式单位使用设备独立像素(dp),而不是 CSS 中的像素(px)。这是因为在不同的设备上,像素密度不同,使用 dp 可以保证应用程序在不同设备上的表现一致。
  3. 样式优先级:RN 中的样式优先级是以组件树为基础的,而不是 CSS 中的继承和优先级。这意味着子组件不能继承父组件的样式,并且相同的样式属性在不同组件上的优先级是相同的。
  4. 布局:RN 中的布局方式是基于盒子模型的,但是有一些不同之处,比如默认情况下,组件是根据其内容自适应宽度和高度的,而不是基于父元素的尺寸来确定自身的大小。

RN中的Flex布局和Web开发中的Flex布局有许多相似之处,但也有一些区别:

  • 默认值不同:RN中的Flex容器默认flexDirection为'column',而Web开发中的默认值为'row'。因此,如果要实现水平排列的元素,需要将RN中容器的flexDirection属性设置为'row'。
  • 相对单位不同:在Web开发中,flex布局中的width和height属性的默认值为auto,可以使用**px 、em、%等相对单位进行设置,而在RN中,可以使用相对单位的只有flex属性。
  • Flex容器不能有margin属性:在Web开发中,Flex容器可以使用margin属性设置外边距,但在RN中不允许。
  • Flex容器不能设置border属性:在RN中,Flex容器不允许设置border属性,但是在Web开发中,Flex容器可以设置border属性。
  • 支持Safe Area:RN中,可以使用SafeAreaView组件来确保元素不会被设备的“安全区域”覆盖。这在Web开发中并不存在。
  1. 样式表:在 RN 中,通常会使用样式表对象来管理组件的样式,而不是在 JSX 中直接编写样式。这种方式可以使样式更易于维护和重用,并且可以使用 JavaScript 动态计算样式值。
  2. 外部资源:在 RN 中,外部资源如图片和字体文件需要使用特殊的方式进行加载,如使用 require 关键字来加载图片。

样式优先级

和 RN中的样式优先级的两个重要的概念:

  1. 组件树:RN中的组件通常是通过嵌套其他组件来构建的。这些嵌套的组件形成了一棵树形结构,其中每个组件都有一个父组件和多个子组件。
  2. 组件样式优先级:当同一个组件中的两个或多个样式属性具有相同的名称但具有不同的值时,RN会使用样式属性的顺序来决定使用哪个值。这个顺序是基于组件树的,并且由组件在树中的位置决定。

举个栗子:

import React from 'react';
import { View, Text } from 'react-native';
import styles from './styles';

export default function App() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Hello world!</Text>
    </View>
  );
}

在上面的示例中,有两个样式对象:styles.containerstyles.text,分别用于应用于<View><Text>组件。如果这两个样式对象中都有一个名为 color 的属性,RN 会使用组件的位置来决定哪个样式属性的值被应用。具体来说,styles.text 中的 color 属性将覆盖 styles.container 中的 color 属性,因为 Text 组件在 View 组件之内。

样式表

RN 中书写样式也与传统 Web 开发时不同,通过 StyleSheet Objects,书写类 CSS 的语法,而且自带属性验证,对于不正确的值会抛错

// 官方的例子
import React from "react";
import { StyleSheet, Text, View } from "react-native";

const App = () => (
  <View style={styles.container}>
    <Text style={styles.title}>React Native</Text>
  </View>
);

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 24,
    backgroundColor: "#eaeaea"
  },
  title: {
    marginTop: 16,
    paddingVertical: 8,
    borderWidth: 4,
    borderColor: "#20232a",
    borderRadius: 6,
    backgroundColor: "#61dafb",
    color: "#20232a",
    textAlign: "center",
    fontSize: 30,
    fontWeight: "bold"
  }
});

export default App; 

导航

RN 的导航是通过库 React Navigation(目前是 v6) 来实现的,RN 中导航与 Web 中导航最大区别,就是没有 URL 这个概念了,所以配置路由的方法与 Web 中根据 URL 映射组件的方法略有不同。

注意:使用 React Navigation 需要我们将代码包裹在 NavigationContainer

import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';

export default function App() {
  return (
    <NavigationContainer>{/* Rest of your app code */}</NavigationContainer>
  );
}

NativeStack

NativeStack Navigator 是 React Navigation v6 中的新导航器,它是一个基于原生平台的导航器。它通过使用原生的导航堆栈来提供更好的性能和可访问性。NativeStack Navigator 是一个堆栈导航器,它允许用户在应用程序中导航到不同的屏幕。

import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

export default App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Stack、BottomTab、Drawer

  • BottomTab 类型,顾名思义就是一般 App 首页底部的按钮对应的页面
  • Drawer 是侧栏弹出的菜单对应的页面。如果需要区分未登录与已登录的页面,可以用 Stack 类型中的 mode 属性来实现。
  • 其余所有的页面,都可放入 Stack 类型中。不过 Stack Navigator 没有 NativeStack Navigator 提供的性能和可访问性

创建对应路由类型的方法如下

import { createStackNavigator } from '@react-navigation/stack';

const Stack = createStackNavigator();

function MyStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={Home} />
      <Stack.Screen name="Notifications" component={Notifications} />
    </Stack.Navigator>
  );
}
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

const Tab = createBottomTabNavigator();

function MyTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Settings" component={SettingsScreen} />
    </Tab.Navigator>
  );
}
import { createDrawerNavigator } from '@react-navigation/drawer';

const Drawer = createDrawerNavigator();

function MyDrawer() {
  return (
    <Drawer.Navigator>
      <Drawer.Screen name="Feed" component={Feed} />
      <Drawer.Screen name="Article" component={Article} />
    </Drawer.Navigator>
  );
}

Screen Component

被配置进上述路由类型中的组件,会被定义为 Screen 组件

Screen 组件是 React Navigation 中用于描述应用程序中单个屏幕的 React 组件。每个屏幕都由 Screen 组件定义,它们包含了组件的名称、组件的属性、以及用于渲染该组件的函数。Screen 组件还可以接收 NavigationProp 对象作为 props,该对象提供了一组用于导航的函数和状态。

Navigation

Navigation API 是 React Navigation 中用于导航的一组函数和状态。Navigation API 可以通过导航器的 NavigationProp 对象进行访问,并可用于实现诸如导航到其他屏幕、在屏幕之间传递数据等功能。Navigation API 中的一些常用函数包括 navigate、goBack、reset、push、pop 等。

function HomeScreen() {
  const navigation = useNavigation();

  return (
    <View>
      <Text>This is the home screen of the app</Text>
      <Button 
        title={'Go to Brent’s profile'}
        onPress={() => navigation.navigate("Profile", { screen: "Brent" })}
      />
    </View>);
}

HeaderBar

HeaderBar 是用于在页面顶部显示标题、导航按钮和其他相关内容的组件。

可以在组件的 options 属性上添加相应的配置,来自定义 HeaderBar。可以使用 headerLeft 属性来配置页面的返回按钮,可以使用 headerRight 属性来配置页面的导航按钮。

function StackScreen() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="Home"
        component={HomeScreen}
        options={{
          title: 'My home',
          headerStyle: {
            backgroundColor: '#f4511e',
          },
          headerTintColor: '#fff',
          headerTitleStyle: {
            fontWeight: 'bold',
          },
        }}
      />
    </Stack.Navigator>
  );
}

使用 HeaderBar 中的按钮有一点需要注意,就是组件传值问题。按钮组件与我们页面组件的数据是不通的,那我们如何在两个组件中传值呢?可以通过 navigation.setOptions 设置后,在另一边使用 route.params 拿值。

// MyComponent
import React from 'react';
import { Button } from 'react-native';
import { useNavigation } from '@react-navigation/native';

const MyComponent = () => {
  const navigation = useNavigation();

  navigation.setOptions({
    headerRight: () => (
      <Button
        onPress={() => {
          navigation.navigate('AnotherScreen', { someParam: 'Hello World!' });
        }}
        title="Go to another screen"
      />
    ),
  });

  return (
    // 页面组件的代码
  );
};

export default MyComponent;




// AnotherScreen
import React from 'react';
import { Text } from 'react-native';
import { useRoute } from '@react-navigation/native';

const AnotherScreen = () => {
  const route = useRoute();

  return (
    <Text>{route.params.someParam}</Text>
  );
};

export default AnotherScreen;

存储与鉴权

对于 RN 端,需要在获取 Token 后存到本地,这时你可能就发现问题了,在 Web 应用中,我们都是将 Token 存在 localStorage 中,而 RN 中没有这个概念,取而代之的是 AsyncStorage 模块。

// 官方例子

import { AsyncStorage } from 'react-native';

// 存数据
_storeData = async () => {
  try {
    await AsyncStorage.setItem(
      '@MySuperStore:key',
      'I like to save it.'
    );
  } catch (error) {
    // Error saving data
  }
};

// 取数据
_retrieveData = async () => {
  try {
    const value = await AsyncStorage.getItem('TASKS');
    if (value !== null) {
      // We have data!!
      console.log(value);
    }
  } catch (error) {
    // Error retrieving data
  }
};