[译]React中的用户认证(登录态管理)

6,596 阅读5分钟

[译]React中的用户认证(登录态管理)

原文地址kentcdodds.com/blog/authen…

本文主要展示在当下 React 应用开发中,怎么使用 ContextHooks 来管理用户的认证(也就是登录态)。

先说结论

下面是本文最终要实现的的简化版,方便大佬们直接看最后的效果:

import React from 'react'
import {useUser} from './context/auth'
import AuthenticatedApp from './authenticated-app'
import UnauthenticatedApp from './unauthenticated-app'
function App() {
  const user = useUser()
  return user ? <AuthenticatedApp /> : <UnauthenticatedApp />
}
export App

嗯,最终的代码大概就长这样。大多数 需要进行用户认证管理的应用,都可以使用类似上面的逻辑来管理用户登录状态。当用户访问我们应用中的某个需要登录后才能访问的页面时,我们可以将用户重定向到登陆页,大多数情况下也是这样做的,除此之外,我们还可以不进行跳转,直接在该页面上展示未登录用户看到的界面。为了提高用户体验,我们也可以这样:

import React from 'react'
import {useUser} from './context/auth'
const AuthenticatedApp = React.lazy(() => import('./authenticated-app'))
const UnauthenticatedApp = React.lazy(() => import('./unauthenticated-app'))
function App() {
  const user = useUser()
  return user ? <AuthenticatedApp /> : <UnauthenticatedApp />
}
export App

亲,代码 懒加载 就实现了:未登录用户访问我们页面,只会加载渲染未登录界面的代码;已登录用户访问同一个页面,同样只会加载渲染已登录界面的代码。

具体在 AuthenticatedAppUnauthenticatedApp 里渲染什么界面,完全是开发者来决定。或许他们会渲染一些 router ,甚至会复用一些公共组件。无论具体渲染什么,我们都不用关心用户的登录态了,因为在渲染这两个组件之一的时候,已经明确知道了用户的登录状态。

怎么一步步实现上面的逻辑

如果你想看看最终整个APP的实现,可以到 这个github 查看。

OK,我们接下来看看怎么一步步来实现上面的逻辑。首先,我们看下我们应用的入口代码:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './app'
import AppProviders from './context'
ReactDOM.render(
  <AppProviders>
    <App />
  </AppProviders>,
  document.getElementById('root'),
)

下面是 AppProviders组件的代码:

import React from 'react'
import {AuthProvider} from './auth-context'
import {UserProvider} from './user-context'
function AppProviders({children}) {
  return (
    <AuthProvider>
      <UserProvider>{children}</UserProvider>
    </AuthProvider>
  )
}
export default AppProviders

OK,我们有两个 Provider: 一个是维护应用的认证状态;另一个是维护当前登录用户的数据。按照字面意思,AppProvider 负责初始化整个APP的数据(如果在localStorage中存在用户认证后的token,那么我们可以直接从token中获取一些用户数据)。另一方面,UserProvider负责将我们对用户数据的一些修改(比如email地址、履历等)保持和服务器端的同步。

完整的auth-context.js 包含一些和本文主题无关的逻辑,因此下面我们来看下简化版的 auth-context.js

import React from 'react'
import {FullPageSpinner} from '../components/lib'
const AuthContext = React.createContext()
function AuthProvider(props) {
  // 如果我们还不确定当前用户是否登录,比如还在请求后端接口查询登录状态,
  // 那么我们就渲染一个全局的加载中,而不是加载真正的页面组件
  if (weAreStillWaitingToGetTheUserData) {
    return <FullPageSpinner />
  }
  const login = () => {} // make a login request
  const register = () => {} // register the user
  const logout = () => {} // clear the token in localStorage and the user data
 
  // 注意:这里我并没有使用 `React.useMemo` 来优化 provider 的 `value`。
  // 因为这是我们应用里最顶级的组件,很少会在这个顶级组件上触发 重新render
  return (
    <AuthContext.Provider value={{data, login, logout, register}} {...props} />
  )
}
const useAuth = () => React.useContext(AuthContext)
export {AuthProvider, useAuth}
// user-context.js 文件里的 `UserProvider` 大概长这样:
// const UserProvider = props => (
//   <UserContext.Provider value={useAuth().data.user} {...props} />
// )
// and the useUser hook is basically this:
// const useUser = () => React.useContext(UserContext)

简化我们应用里的认证管理的关键点是:

负责维护用户登录态的组件,在获取到当前用户的登录状态之前,不会渲染页面的主体内容,可以渲染一个加载中的全局loading。只有当从服务器端获取到用户的登录状态之后,才去渲染页面的主体:已登录就渲染登录后的组件;未登录渲染未登录的。

结论

在实际开发中,很多APP面临的场景都是不同的。如果你采用了服务端渲染技术(SSR),那么你很可能不需要一个加载中的loading,因为你在服务端已经明确知道了,当前用户是否已登录。即使在这个场景下,同样可以将用户的登录状态提出到全局的 Provider 来管理,这样会增强我们代码的可维护性。

PS

有些同学问到了相同的问题:如果我们的应用,已登录用户和未登录用户看到的很多界面都相同(比如twitter),而不是像gmail那样,已登录用户和未登录用户看到的完全不同,我们应该怎么来维护用户登录状态呢?

如果是这种情况,那么代码里很多组件,都会用到 useUser这个hook,为了能在一个公共组件中,根据是否登录而执行不同逻辑。因为我们的公共组件可能值关心用户是否登录,你甚至可以再封装出一个 useIsAuthenticated hook,返回一个 boolean 值,表示当前用户是否已登录。多亏了 ContextHooks,要实现这样的逻辑非常的简单。