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

标签 reactjs react-router apollo react-apollo apollo-client

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

  1. 允许用户查看的页面基于其用户状态,可以从服务器获取,也可以从缓存中读取。用户状态本质上是一组标志,我们用来了解用户在 channel 中的位置。示例标志:isLoggedInisOnboardedisWaitlisted

  2. 如果用户的状态不允许他们进入该页面,则该页面甚至不应开始呈现。例如,如果您不是 isWaitlisted,则您不应看到等候名单页面。当用户意外发现自己位于这些页面时,应该将他们重定向到适合其状态的页面。

  3. 重定向也应该是动态的。例如,假设您在 isLoggedIn 之前尝试查看您的用户个人资料。然后我们需要将您重定向到登录页面。但是,如果您是 isLoggedIn 但不是 isOnboarded,我们仍然不希望您看到自己的个人资料。因此,我们希望将您重定向到入门页面。

  4. 所有这些都需要在路线级别上进行。页面本身应该不知道这些权限和重定向。

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

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

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

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

此外,我无法使用可以注入(inject)计算重定向路径所需的 props 的 HOC 来包装 PrivateRoute,因为这不会让我将它用作 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'
  })`

  hoistNonReactStatics(Private, WrappedComponent)

  // ...and returns a new component wrapping the parameter component
  return Private
}

export default privateRoute

那么你只需要更改导出路由的方式:

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

您可以像今天在react-router中一样使用该路由:

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

重定向逻辑

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

  1. 如何确定用户是否已登录、已加入、已列入候补名单等。
  2. 您希望负责重定向到哪个组件。

处理用户状态

由于您使用 Apollo,您可能只想使用 graphql 来获取 HOC 中的数据:

return graphql(gql`
  query ...
`)(Private)

然后您可以修改 Private 组件来获取这些 Prop :

class Private extends Component {
  componentDidMount() {
    const {
      userStatus: {
        isLoggedIn,
        isOnboarded,
        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,
        isOnboarded,
        isWaitlisted
      },
      ...passThroughProps
    } = this.props

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

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

重定向到哪里

您可以在几个不同的地方处理这个问题。

简单方法:路由是静态的

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

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

组件负责

如果您希望 MyRoute 组件确定路径,您只需向 privateRoute 函数添加一些额外的参数,然后在导出 时将它们传入即可我的路线

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

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

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

路线负责

如果您希望能够从路由传递路径,则需要在 Private 中接收这些属性

class Private extends Component {
  componentDidMount() {
    const {
      userStatus: {
        isLoggedIn,
        isOnboarded,
        isWaitlisted
      },
      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,
        isOnboarded,
        isWaitlisted
      },
      // we don't care about these for rendering, but we don't want to pass them to WrappedComponent
      pathIfNotLoggedIn,
      pathIfNotOnboarded,
      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,就像我们在路由负责时所做的那样。

这使您可以灵活地做出决定。请注意,将路由作为 props 传递将优先于从组件传递到 HOC。

现在大家在一起

下面是一个结合了上面所有概念的片段,用于最终呈现 HOC:

const privateRoute = ({
  requireLoggedIn = false,
  requireOnboarded = false,
  requireWaitlisted = false,
  pathIfNotLoggedIn = '/login',
  pathIfNotOnboarded = '/onboarding',
  pathIfNotWaitlisted = '/waitlist'
} = {}) => WrappedComponent => {
  class Private extends Component {
    componentDidMount() {
      const {
        userStatus: {
          isLoggedIn,
          isOnboarded,
          isWaitlisted
        },
        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,
          isOnboarded,
          isWaitlisted
        },
        pathIfNotLoggedIn,
        pathIfNotOnboarded,
        pathIfNotWaitlisted,
        ...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,
    pathIfNotOnboarded,
    pathIfNotWaitlisted
  }
  
  Private.displayName = `Private(${
    WrappedComponent.displayName ||
    WrappedComponent.name ||
    'Component'
  })`

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

export default privateRoute

<小时/>

我正在使用hoist-non-react-statics正如 the official documentation 中所建议的.

关于reactjs - 如何使用Apollo Client + React Router实现私有(private)路由和根据用户状态重定向?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48692649/

相关文章:

graphql - 是否可以有部分联合网关?

reactjs - 使用 GraphQL 在 NextJS 中呈现比之前呈现更多的钩子(Hook)

javascript - 无法将 react 模块加载为节点模块

javascript - React-Router 无法创建子路由

reactjs - 如何使用React Router生成站点地图

reactjs - React router History.push 回退到 404 路由

reactjs - Apollo 本地状态 - 查询具有未知键的对象

reactjs - 如何在 NextJS 中向 Router.push 添加查询参数?

reactjs - 如何正确测试 React 组件方法

javascript - Navlink 导致 URL 更改但页面未加载