RN路由-React Navigation--Authentication flows

2,044 阅读6分钟

引言

本来打算将React Navigation-Guides这一章内容集中写在这篇文章内的,但由于篇幅太长,阅读的时候很费劲,所以这里将Guides这一章的内容分篇来记录。接下来,我们来看看Authentication flows!

Authentication flows(即页面的权限控制)

大多数应用程序都要求用户以某种方式进行身份验证,才能访问与用户相关的数据或其他私人内容。通常,流程如下所示:

  • 用户打开应用程序。
  • 应用程序从持久存储(例如,AsyncStorage)加载一些身份验证状态。
  • 加载状态后,将向用户显示身份验证屏幕或主应用程序,具体取决于是否加载了有效的身份验证状态。
  • 当用户注销时,我们清除身份验证状态并将其发送回身份验证页面。

注意:我们说“身份验证页面”是因为通常有不止一个。你可能有一个主页面,有一个用户名和密码字段,另一个是“忘记密码”,另一个设置为注册。

1 我们需要什么

我们希望验证流的行为是:当用户登录时,我们希望丢弃验证流的状态并卸载与身份验证相关的所有页面;当我们按下硬件后退按钮时,我们希望无法返回到身份验证流。

2 如何做

我们可以根据某些条件定义不同的页面。例如,如果用户已登录,我们可以定义主页、配置文件、设置等。如果用户未登录,则可以定义登录和注册页面。

例如:

isSignedIn ? (
  <>
    <Stack.Screen name="Home" component={HomeScreen} />
    <Stack.Screen name="Profile" component={ProfileScreen} />
    <Stack.Screen name="Settings" component={SettingsScreen} />
  </>
) : (
  <>
    <Stack.Screen name="SignIn" component={SignInScreen} />
    <Stack.Screen name="SignUp" component={SignUpScreen} />
  </>
)

当我们定义这样的页面时,当isSignedIn为true时,React Navigation将只看到Home、Profile和Settings页面,而当它为false时,React Navigation将看到SignIn和SignUp页面。这使得在用户未登录时无法导航到Home、Profile和Settings页面,也无法在用户登录时导航到登录和注册页面。

这种模式已经被其他路由库(如React Router)使用了很长一段时间,通常被称为“受保护路由”。在这里,我们需要用户登录的页面是“受保护的”,如果用户没有登录,则无法通过其他方式导航。

当isSignedIn变量的值发生变化时,这种魔力就会发生。比如说,最初的isSignedIn是false的。这意味着,将显示登录或注册页面。用户登录后,isSignedIn的值将变为true。React Navigation将看到登录和注册页面不再被定义,因此它将删除它们。然后它会自动显示主页面,因为这是isSignedIn为true时定义的第一个页面。

需要注意的是,使用这种设置时,不需要通过调用手动导航到主页面navigation.navigate('Home')。当isSignedIn变为true时,React Navigation将自动导航到主页面。

这利用了React导航中的一个新特性:能够根据属性或状态动态定义和更改导航器的页面定义。该示例显示了堆栈导航器,但您可以对任何导航器使用相同的方法。

通过基于变量有条件地定义不同的页面,我们可以以一种简单的方式实现验证流,这种方式不需要额外的逻辑来确保显示正确的页面。

3 定义我们的页面

在navigator中,我们可以有条件地定义适当的页面。对于我们的例子,假设我们有3个页面:

  • SplashScreen-这将在我们恢复token时显示一个启动或加载页面。
  • SignInScreen-这是当用户尚未登录时显示的页面(我们找不到token)。
  • HomeScreen-如果用户已经登录,这是我们显示的页面。

因此我们的导航可以这样:

if (state.isLoading) {
  // We haven't finished checking for the token yet
  return <SplashScreen />;
}

return (
  <Stack.Navigator>
    {state.userToken == null ? (
      // No token found, user isn't signed in
      <Stack.Screen
        name="SignIn"
        component={SignInScreen}
        options={{
          title: 'Sign in',
          // When logging out, a pop animation feels intuitive
          // You can remove this if you want the default 'push' animation
          animationTypeForReplace: state.isSignout ? 'pop' : 'push',
        }}
      />
    ) : (
      // User is signed in
      <Stack.Screen name="Home" component={HomeScreen} />
    )}
  </Stack.Navigator>
);

在上面的代码片段中,isload意味着我们仍在检查是否有token。这通常可以通过检查AsyncStorage中是否有token并验证token来完成。在我们得到token之后,如果它是有效的,我们需要设置userToken。我们还有另一个名为isSignout的状态,在注销时有一个不同的动画。

需要注意的是,我们根据这些状态变量有条件地定义页面:

  • 只有在userToken为null(用户未登录)时才定义登录页面
  • 只有在userToken为非null(用户已登录)时才定义主页面

这里,我们有条件地为每个案例定义一个页面。但也可以定义多个页面。例如,当用户未登录时,您可能还需要定义密码重置、注册等页面。同样,对于登录后可访问的页面,您可能有多个页面。我们可以利用React.Fragment定义多个屏幕:

state.userToken == null ? (
  <>
    <Stack.Screen name="SignIn" component={SignInScreen} />
    <Stack.Screen name="SignUp" component={SignUpScreen} />
    <Stack.Screen name="ResetPassword" component={ResetPassword} />
  </>
) : (
  <>
    <Stack.Screen name="Home" component={HomeScreen} />
    <Stack.Screen name="Profile" component={ProfileScreen} />
  </>
);

4 实现还原token的逻辑

注意:以下只是一个示例,说明如何在应用程序中实现身份验证逻辑。你不需要照原样去做。

从前面的片段可以看出,我们需要3个状态变量:

  • isLoading-当我们试图检查是否已经在AsyncStorage中保存了token时,我们将其设置为true
  • isSignout-我们在用户注销时将其设置为true,否则设置为false
  • userToken-用户的token。如果非空,则假定用户已登录,否则没有登录。

因此我们需要:

  • 添加一些恢复token、登录和注销的逻辑
  • 向其他组件公开登录和注销的方法

在本指南中我们将使用React.useReducer以及React.useContext。但是如果您使用的是一个状态管理库,比如Redux或Mobx,那么您可以将它们用于此功能。事实上,在更大的应用程序中,全局状态管理库更适合存储身份验证token。您可以将相同的方法应用于您的状态管理库。

首先,我们需要为auth创建一个上下文,在这里我们可以公开必要的方法:

import * as React from 'react';

const AuthContext = React.createContext();

我们的组件可以这样:

import * as React from 'react';
import AsyncStorage from '@react-native-community/async-storage';

export default function App({ navigation }) {
  const [state, dispatch] = React.useReducer(
    (prevState, action) => {
      switch (action.type) {
        case 'RESTORE_TOKEN':
          return {
            ...prevState,
            userToken: action.token,
            isLoading: false,
          };
        case 'SIGN_IN':
          return {
            ...prevState,
            isSignout: false,
            userToken: action.token,
          };
        case 'SIGN_OUT':
          return {
            ...prevState,
            isSignout: true,
            userToken: null,
          };
      }
    },
    {
      isLoading: true,
      isSignout: false,
      userToken: null,
    }
  );

  React.useEffect(() => {
    // Fetch the token from storage then navigate to our appropriate place
    const bootstrapAsync = async () => {
      let userToken;

      try {
        userToken = await AsyncStorage.getItem('userToken');
      } catch (e) {
        // Restoring token failed
      }

      // After restoring token, we may need to validate it in production apps

      // This will switch to the App screen or Auth screen and this loading
      // screen will be unmounted and thrown away.
      dispatch({ type: 'RESTORE_TOKEN', token: userToken });
    };

    bootstrapAsync();
  }, []);

  const authContext = React.useMemo(
    () => ({
      signIn: async data => {
        // In a production app, we need to send some data (usually username, password) to server and get a token
        // We will also need to handle errors if sign in failed
        // After getting token, we need to persist the token using `AsyncStorage`
        // In the example, we'll use a dummy token

        dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
      },
      signOut: () => dispatch({ type: 'SIGN_OUT' }),
      signUp: async data => {
        // In a production app, we need to send user data to server and get a token
        // We will also need to handle errors if sign up failed
        // After getting token, we need to persist the token using `AsyncStorage`
        // In the example, we'll use a dummy token

        dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
      },
    }),
    []
  );

  return (
    <AuthContext.Provider value={authContext}>
      <Stack.Navigator>
        {state.userToken == null ? (
          <Stack.Screen name="SignIn" component={SignInScreen} />
        ) : (
          <Stack.Screen name="Home" component={HomeScreen} />
        )}
      </Stack.Navigator>
    </AuthContext.Provider>
  );
}

5 填充其他组件

我们将不讨论如何实现身份验证页面的文本输入和按钮,这超出了导航范围。我们只需要填充一些占位符内容。

function SignInScreen() {
  const [username, setUsername] = React.useState('');
  const [password, setPassword] = React.useState('');

  const { signIn } = React.useContext(AuthContext);

  return (
    <View>
      <TextInput
        placeholder="Username"
        value={username}
        onChangeText={setUsername}
      />
      <TextInput
        placeholder="Password"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      <Button title="Sign in" onPress={() => signIn({ username, password })} />
    </View>
  );
}

上一章节:RN路由-React Navigation--Drawer navigation

参考文档:React Navigation - Authentication flows