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

时间:2018-02-08 18:42:42

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

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

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

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

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

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

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

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

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

    这是我目前的做法。这不起作用,因为getRedirectPath所需的数据位于OnboardingPage component

    另外,我不能用HOC包装PrivateRoute,它可以注入计算重定向路径所需的道具,因为这不会让我将它用作Switch React Router组件的子节点,因为它不再是路由。

    <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}
    />
    

5 个答案:

答案 0 :(得分:9)

一般方法

我会创建一个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} />

重定向逻辑

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

  1. 如何确定用户是否已登录,登机,等候等等。
  2. 您希望负责 重定向到哪个
  3. 处理用户状态

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

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

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

    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函数中添加一些额外参数,然后在导出MyRoute时将其传入。

    const privateRoute = ({
      requireLogedIn = 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&amp; 3

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

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

    这使您可以灵活地决定当中。请注意,将路径作为道具传递将优先于从组件传递到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'
      })`
    
      return hoistNonReactStatics(
        graphql(gql`
          query ...
        `)(Private),
        WrappedComponent
      )
    }
    
    export default privateRoute

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

答案 1 :(得分:1)

我认为你需要稍微改变你的逻辑。类似的东西:

<Route path="/onboarding" render={renderProps=>
   <CheckAuthorization authorized={OnBoardingPage} renderProps={renderProps} />
}/>

答案 2 :(得分:1)

我个人用来建立我的私人路线:

const renderMergedProps = (component, ...rest) => {
  const finalProps = Object.assign({}, ...rest);
  return React.createElement(component, finalProps);
};

const PrivateRoute = ({
  component, redirectTo, path, ...rest
}) => (
  <Route
    {...rest}
    render={routeProps =>
      (loggedIn() ? (
        renderMergedProps(component, routeProps, rest)
      ) : (
        <Redirect to={redirectTo} from={path} />
      ))
    }
  />
);

在这种情况下,loggedIn()是一个简单的函数,如果记录了用户,则返回true(取决于您处理用户会话的方式),您可以像这样创建每个私有路由。

然后你可以在Switch中使用它:

<Switch>
    <Route path="/login" name="Login" component={Login} />
    <PrivateRoute
       path="/"
       name="Home"
       component={App}
       redirectTo="/login"
     />
</Switch>

PrivateRoute的所有子路由首先需要检查用户是否已登录。

最后一步是根据所需状态嵌套路线。

答案 3 :(得分:0)

你必须使用没有'react-graphql'HOC的ApolloClient  1.获取ApolloClient的实例
 2.火灾查询
 3. Query返回数据渲染加载..
 4.根据数据检查和授权路线  5.返回适当的组件或重定向。

这可以通过以下方式完成:

import Loadable from 'react-loadable'
import client from '...your ApolloClient instance...'

const queryPromise = client.query({
        query: Storequery,
        variables: {
            name: context.params.sellername
        }
    })
const CheckedComponent = Loadable({
  loading: LoadingComponent,
  loader: () => new Promise((resolve)=>{
       queryPromise.then(response=>{
         /*
           check response data and resolve appropriate component.
           if matching error return redirect. */
           if(response.data.userStatus.isLoggedIn){
            resolve(ComponentToBeRendered)
           }else{
             resolve(<Redirect to={somePath}/>)
           }
       })
   }),
}) 
<Route path="/onboarding" component={CheckedComponent} />

相关API参考: https://www.apollographql.com/docs/react/reference/index.html

答案 4 :(得分:0)

如果您使用的是apollo react client,您还可以导入Query @apollo/components并在您的私有路由中像这样使用它:

    <Query query={fetchUserInfoQuery(moreUserInfo)}>
      {({ loading, error, data: userInfo = {} }: any) => {
        const isNotAuthenticated = !loading && (isEmpty(userInfo) || !userInfo.whoAmI);

        if (isNotAuthenticated || error) {
          return <Redirect to={RoutesPaths.Login} />;
        }
        const { whoAmI } = userInfo;

        return <Component user={whoAmI} {...renderProps} />;
      }}
    </Query>

其中isEmpty仅检查给定对象是否为空:

const isEmpty = (object: any) => object && Object.keys(object).length === 0