reactjs – 如何使用Apollo Client React Router根据用户状态实现私有路由和重定向?

我正在使用React Router 4进行路由,使用Apollo Client进行数据获取&缓存.我需要根据以下标准实施PrivateRoute和重定向解决方案:

>允许用户查看的页面基于用户状态,可以从服务器获取,也可以从缓存中读取.用户状态本质上是一组标志,用于了解用户在我们的渠道中的位置.示例标志:isLoggedIn,isOnboarded,isWaitlisted等.
>如果用户的状态不允许他们在该页面上,则不应该开始呈现页面.例如,如果您不是isWaitlisted,则不应该看到等待列表页面.当用户意外地发现自己在这些页面上时,应将其重定向到适合其状态的页面.
>重定向也应该是动态的.例如,假设您在isLoggedIn之前尝试查看用户配置文件.然后我们需要将您重定向到登录页面.但是,如果您是isLoggedIn但不是isOnboarded,我们仍然不希望您看到您的个人资料.因此,我们希望将您重定向到新手入门页面.
>所有这些都需要在路线层面上进行.页面本身应该不知道这些权限&重定向.

总之,我们需要一个给用户状态数据的库,可以

>计算用户是否可以在某个页面上
>计算需要动态重定向的位置
>在渲染任何页面之前执行这些操作
>在路线级别执行这些操作

我已经在开发一个通用库,但它现在有它的缺点.我正在寻求关于如何解决这个问题的意见,以及是否有既定的模式来实现这一目标.

这是我目前的做法.这不起作用,因为getRedirectPath需要的数据位于OnboardingPage组件中.

此外,我无法将PrivateRoute包装为可以注入计算重定向路径所需的道具的HOC,因为这不会让我将其用作Switch React Router组件的子项,因为它不再是Route.

<PrivateRoute
  exact
  path="/onboarding"
  isRender={(props) => {
    return props.userStatus.isLoggedIn && props.userStatus.isWaitlistApproved;
  }}
  getRedirectPath={(props) => {
    if (!props.userStatus.isLoggedIn) return '/login';
    if (!props.userStatus.isWaitlistApproved) return '/waitlist';
  }}
  component={OnboardingPage}
/>
一般的做法

我会创建一个HOC来处理所有页面的逻辑.

// privateRoute is a function...
const privateRoute = ({
  // ...that takes optional boolean parameters...
  requireLoggedIn = false,requireOnboarded = false,requireWaitlisted = false
// ...and returns a function that takes a component...
} = {}) => WrappedComponent => {
  class Private extends Component {
    componentDidMount() {
      // redirect logic
    }

    render() {
      if (
        (requireLoggedIn && /* user isn't logged in */) ||
        (requireOnboarded && /* user isn't onboarded */) ||
        (requireWaitlisted && /* user isn't waitlisted */) 
      ) {
        return null
      }

      return (
        <WrappedComponent {...this.props} />
      )
    }
  }

  Private.displayName = `Private(${
    WrappedComponent.displayName ||
    WrappedComponent.name ||
    'Component'
  })`

  // ...and returns a new component wrapping the parameter component
  return hoistNonReactStatics(Private,WrappedComponent)
}

export default privateRoute

然后,您只需要更改导出路线的方式:

export default privateRoute({ requireLoggedIn: true })(MyRoute);

并且您可以像在今天的react-router中那样使用该路由:

<Route path="/" component={MyPrivateRoute} />

重定向逻辑

如何设置此部分取决于几个因素:

>如何确定用户是否已登录,登机,等候等.
>您希望在哪个组件中负责重定向到哪里.

处理用户状态

既然你正在使用Apollo,你可能只想使用graphql来获取你的HOC中的数据:

return hoistNonReactStatics(
  graphql(gql`
    query ...
  `)(Private),WrappedComponent
)

然后你可以修改私有组件来获取这些道具:

class Private extends Component {
  componentDidMount() {
    const {
      userStatus: {
        isLoggedIn,isWaitlisted
      }
    } = this.props

    if (requireLoggedIn && !isLoggedIn) {
      // redirect somewhere
    } else if (requireOnboarded && !isOnboarded) {
      // redirect somewhere else
    } else if (requireWaitlisted && !isWaitlisted) {
      // redirect to yet another location
    }
  }

  render() {
    const {
      userStatus: {
        isLoggedIn,isWaitlisted
      },...passThroughProps
    } = this.props

    if (
      (requireLoggedIn && !isLoggedIn) ||
      (requireOnboarded && !isOnboarded) ||
      (requireWaitlisted && !isWaitlisted) 
    ) {
      return null
    }

    return (
      <WrappedComponent {...passThroughProps} />
    )
  }
}

在哪里重定向

您可以使用几个不同的地方来处理这个问题.

简单方法:路线是静态的

如果用户未登录,您总是希望路由到/ login?return = ${currentRoute}.

在这种情况下,您可以在componentDidMount中对这些路由进行硬编码.完成.

该组件负责

如果您希望MyRoute组件确定路径,您可以在privateRoute函数中添加一些额外的参数,然后在导出MyRoute时将其传入.

const privateRoute = ({
  requireLogedIn = false,pathIfNotLoggedIn = '/a/sensible/default',// ...
}) // ...

然后,如果要覆盖默认路径,请将导出更改为:

export default privateRoute({ 
  requireLoggedIn: true,pathIfNotLoggedIn: '/a/specific/page'
})(MyRoute)

路线是负责任的

如果您希望能够从路由传递路径,您将希望在私有中接收这些路径

class Private extends Component {
  componentDidMount() {
    const {
      userStatus: {
        isLoggedIn,pathIfNotLoggedIn,pathIfNotOnboarded,pathIfNotWaitlisted
    } = this.props

    if (requireLoggedIn && !isLoggedIn) {
      // redirect to `pathIfNotLoggedIn`
    } else if (requireOnboarded && !isOnboarded) {
      // redirect to `pathIfNotOnboarded`
    } else if (requireWaitlisted && !isWaitlisted) {
      // redirect to `pathIfNotWaitlisted`
    }
  }

  render() {
    const {
      userStatus: {
        isLoggedIn,// we don't care about these for rendering,but we don't want to pass them to WrappedComponent
      pathIfNotLoggedIn,pathIfNotWaitlisted,...passThroughProps
    } = this.props

    if (
      (requireLoggedIn && !isLoggedIn) ||
      (requireOnboarded && !isOnboarded) ||
      (requireWaitlisted && !isWaitlisted) 
    ) {
      return null
    }

    return (
      <WrappedComponent {...passThroughProps} />
    )
  }
}

Private.propTypes = {
  pathIfNotLoggedIn: PropTypes.string
}

Private.defaultProps = {
  pathIfNotLoggedIn: '/a/sensible/default'
}

然后您的路线可以重写为:

<Route path="/" render={props => <MyPrivateComponent {...props} pathIfNotLoggedIn="/a/specific/path" />} />

结合选项2& 3

(这是我喜欢使用的方法)

您还可以让组件和路径选择负责人.你只需要为路径添加privateRoute参数,就像我们让组件决定一样.然后使用这些值作为defaultProps,就像我们在路由负责时所做的那样.

这使您可以灵活地决定当中.请注意,将路径作为道具传递将优先于从组件传递到HOC.

现在都在一起了

这里有一个片段,结合了上面的所有概念,最终对HOC采取了以下措施:

const privateRoute = ({
  requireLoggedIn = false,requireWaitlisted = false,pathIfNotLoggedIn = '/login',pathIfNotOnboarded = '/onboarding',pathIfNotWaitlisted = '/waitlist'
} = {}) => WrappedComponent => {
  class Private extends Component {
    componentDidMount() {
      const {
        userStatus: {
          isLoggedIn,isWaitlisted
        },pathIfNotWaitlisted
      } = this.props

      if (requireLoggedIn && !isLoggedIn) {
        // redirect to `pathIfNotLoggedIn`
      } else if (requireOnboarded && !isOnboarded) {
        // redirect to `pathIfNotOnboarded`
      } else if (requireWaitlisted && !isWaitlisted) {
        // redirect to `pathIfNotWaitlisted`
      }
    }

    render() {
      const {
        userStatus: {
          isLoggedIn,...passThroughProps
      } = this.props

      if (
        (requireLoggedIn && !isLoggedIn) ||
        (requireOnboarded && !isOnboarded) ||
        (requireWaitlisted && !isWaitlisted) 
      ) {
        return null
      }
    
      return (
        <WrappedComponent {...passThroughProps} />
      )
    }
  }

  Private.propTypes = {
    pathIfNotLoggedIn: PropTypes.string,pathIfNotOnboarded: PropTypes.string,pathIfNotWaitlisted: PropTypes.string
  }

  Private.defaultProps = {
    pathIfNotLoggedIn,pathIfNotWaitlisted
  }
  
  Private.displayName = `Private(${
    WrappedComponent.displayName ||
    WrappedComponent.name ||
    'Component'
  })`

  return hoistNonReactStatics(
    graphql(gql`
      query ...
    `)(Private),WrappedComponent
  )
}

export default privateRoute

我按照the official documentation的建议使用hoist-non-react-statics.

相关文章

react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接...
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc ...