【问题标题】:Web app taking too long to determine whether user is authenticated or notWeb 应用程序花费太长时间来确定用户是否经过身份验证
【发布时间】:2020-04-26 23:21:56
【问题描述】:

背景资料

我刚刚开始使用/学习如何使用 Next.js,我遇到了一个问题,即我的用户身份验证逻辑使我的组件需要很长时间才能在页面上呈现。我相信我遗漏了一些非常基本的东西,我不确定它是否与 Next.js 相关,或者与我如何在我的应用程序中处理用户状态有关。

我正在使用 Firebase 的 Google 身份验证来处理我的用户登录。

我将在我的问题中引用的代码存在于以下存储库中: https://github.com/myamazingthrowaway/nextjswebsite

可以在此处找到该应用的现场演示:
https://nextjswebsite-kappa-sand.now.sh/

(它使用跨站点 cookie 来处理 firebase google 登录 - 我不知道如何更改此默认行为,因此如果第一次无法正常工作,请确保您的浏览器允许跨站点 cookie)

我的身份验证逻辑基于以下存储库:
https://github.com/taming-the-state-in-react/nextjs-redux-firebase-authentication

我的网络应用是用create-next-app 制作的。

问题

当用户访问我的网站时,侧边栏组件和其他依赖于用户登录状态的组件不会在页面加载时立即出现。它们在初始页面呈现后出现一段时间。这是一个明显的延迟,我的 chrome 选项卡上没有“加载指示器”表明 dom 仍在构建中

这是预期的行为吗?

这个问题也可以在以下网站上看到(用谷歌登录证明了我的意思)。

重现步骤:
1。转至:https://kage.saltycrane.com/
2。按“登录”
3。按“使用 Google 登录”

您将被重定向到谷歌登录页面,选择一个帐户等(登录)

然后,您将被重定向回第 1 步中的站点,顶部的菜单栏仍然保持“登录”状态.. 一两分钟,然后它会更改为您的电子邮件地址。

为什么会这样?

(上面网页背后的代码在这里:https://github.com/saltycrane/kage

我的代码

在我的_app.js 文件中,我有一个“Shell”组件,用于处理整个网络应用程序的侧边栏和导航栏。它接受要在侧边栏等范围内呈现的子组件。也许这不是处理应用程序如何流动的最佳方式(对于如何改进这一点的建议会非常高兴)。

_app.js 文件如下所示:

import React from "react";
import App from "next/app";

import CssBaseline from "@material-ui/core/CssBaseline";
import { ThemeProvider } from "@material-ui/styles";

import { Provider } from "react-redux";
import withRedux from "next-redux-wrapper";

import initStore from "../src/store";
import theme from "../src/theme";

import Shell from "../src/components/Shell";

class EnhancedApp extends App {
  static async getInitialProps({ Component, ctx }) {
    return {
      pageProps: Component.getInitialProps
        ? await Component.getInitialProps(ctx)
        : {}
    };
  }

  componentDidMount() {
    const jssStyles = document.querySelector("#jss-server-side");

    if (jssStyles) {
      jssStyles.parentNode.removeChild(jssStyles);
    }
  }

  render() {
    const { Component, pageProps, store } = this.props;
    return (
      <>
        <Provider store={store}>
          <ThemeProvider theme={theme}>
            <title>Next.js</title>
            <CssBaseline />
            <Shell>
              <Component {...pageProps} />
            </Shell>
          </ThemeProvider>
        </Provider>
      </>
    );
  }
}

export default withRedux(initStore)(EnhancedApp);

我的Shell 组件如下所示:

import React from "react";
import Router from "next/router";
import { connect } from "react-redux";

import {
  Drawer,
  List,
  Divider,
  ListItem,
  ListItemIcon,
  ListItemText,
  Hidden,
  AppBar,
  Toolbar,
  IconButton,
  Button
} from "@material-ui/core";
import { ProfileIcon } from "../index";

import MonetizationOnOutlinedIcon from "@material-ui/icons/MonetizationOnOutlined";
import AccountBalanceWalletRoundedIcon from "@material-ui/icons/AccountBalanceWalletRounded";
import AccountBoxRoundedIcon from "@material-ui/icons/AccountBoxRounded";
import VpnKeyRoundedIcon from "@material-ui/icons/VpnKeyRounded";
import ExitToAppRoundedIcon from "@material-ui/icons/ExitToAppRounded";
import MenuIcon from "@material-ui/icons/Menu";

import { makeStyles } from "@material-ui/core/styles";

import * as routes from "../../constants/routes";
import { auth } from "../../firebase/firebase";

const drawerWidth = 180;

const useStyles = makeStyles(theme => ({
  content: {
    flexGrow: 1,
    padding: theme.spacing(3)
  },
  root: {
    display: "flex"
  },
  container: {
    flexGrow: 1
  },
  toolbar: theme.mixins.toolbar,
  drawer: {
    [theme.breakpoints.up("md")]: {
      width: drawerWidth,
      flexShrink: 0
    }
  },
  drawerPaper: {
    width: drawerWidth
  },
  appBar: {
    background: "linear-gradient(45deg,  #FF8E53 30%, #ff4d73 90%)",
    marginLeft: drawerWidth,
    [theme.breakpoints.up("md")]: {
      width: `calc(100% - ${drawerWidth}px)`
    }
  },
  logoContainer: {
    background: "linear-gradient(45deg, #ff4d73 30%, #FF8E53 90%)",
    justifyContent: "center",
    flexDirection: "column",
    height: "15rem"
  },
  menuButton: {
    marginRight: theme.spacing(2),
    [theme.breakpoints.up("md")]: {
      display: "none"
    }
  },
  rightAlign: {
    marginLeft: "auto",
    marginRight: -12,
    cursor: "pointer"
  },
  hoverCursor: {
    cursor: "pointer"
  }
}));

const Shell = ({ children, authUser }) => {
  const classes = useStyles();
  const [mobileOpen, setMobileOpen] = React.useState(false);

  const handleGoToEarnPage = () => {
    Router.push(routes.EARN);
    if (mobileOpen) handleDrawerToggle();
  };

  const handleGoToSignInPage = () => {
    Router.push(routes.SIGN_IN);
    if (mobileOpen) handleDrawerToggle();
  };

  const handleGoToWithdrawPage = () => {
    Router.push(routes.WITHDRAW);
    if (mobileOpen) handleDrawerToggle();
  };

  const handleGoToProfilePage = () => {
    Router.push(routes.PROFILE);
    if (mobileOpen) handleDrawerToggle();
  };

  const handleDrawerToggle = () => {
    setMobileOpen(!mobileOpen);
  };

  const handleGoToHomePage = () => {
    Router.push(routes.LANDING);
    if (mobileOpen) handleDrawerToggle();
  };

  const handleSignOut = () => {
    auth.signOut();
    if (mobileOpen) handleDrawerToggle();
  };

  const drawer = (
    <>
      <AppBar position="static">
        <Toolbar className={classes.logoContainer}>
          <img
            src="/images/logo/logo.png"
            alt="my logo"
            height="120rem"
            onClick={handleGoToHomePage}
            className={classes.hoverCursor}
          />
        </Toolbar>
      </AppBar>

      <List>
        <ListItem button key="Earn" href="/earn" onClick={handleGoToEarnPage}>
          <ListItemIcon>
            <MonetizationOnOutlinedIcon />
          </ListItemIcon>
          <ListItemText primary="Earn" />
        </ListItem>

        <ListItem
          button
          key="Withdraw"
          href="/withdraw"
          onClick={handleGoToWithdrawPage}
        >
          <ListItemIcon>
            <AccountBalanceWalletRoundedIcon />
          </ListItemIcon>
          <ListItemText primary="Withdraw" />
        </ListItem>

        <Divider variant="middle" />
        {!authUser && (
          <List>
            <ListItem
              button
              key="Sign In"
              href="/signin"
              onClick={handleGoToSignInPage}
            >
              <ListItemIcon>
                <VpnKeyRoundedIcon />
              </ListItemIcon>
              <ListItemText primary="Sign In" />
            </ListItem>
          </List>
        )}

        {authUser && (
          <List>
            <ListItem
              button
              key="Profile"
              href="/profile"
              onClick={handleGoToProfilePage}
            >
              <ListItemIcon>
                <AccountBoxRoundedIcon />
              </ListItemIcon>
              <ListItemText primary="Profile" />
            </ListItem>

            <ListItem button key="Sign Out" onClick={handleSignOut}>
              <ListItemIcon>
                <ExitToAppRoundedIcon />
              </ListItemIcon>
              <ListItemText primary="Sign Out" />
            </ListItem>
          </List>
        )}
      </List>
    </>
  );

  return (
    <div className={classes.root}>
      <AppBar position="fixed" className={classes.appBar}>
        <Toolbar>
          <IconButton
            color="inherit"
            aria-label="open drawer"
            edge="start"
            onClick={handleDrawerToggle}
            className={classes.menuButton}
          >
            <MenuIcon />
          </IconButton>
          <div className={classes.rightAlign}>
            {authUser && <ProfileIcon className={classes.hoverCursor} />}
            {!authUser && (
              <Button color="inherit" onClick={handleGoToSignInPage}>
                Sign In
              </Button>
            )}
          </div>
        </Toolbar>
      </AppBar>

      <nav className={classes.drawer} aria-label="sidebar">
        <Hidden mdUp>
          <Drawer
            variant="temporary"
            anchor={classes.direction === "rtl" ? "right" : "left"}
            open={mobileOpen}
            onClose={handleDrawerToggle}
            classes={{
              paper: classes.drawerPaper
            }}
            ModalProps={{
              keepMounted: true // Better open performance on mobile.
            }}
          >
            {drawer}
          </Drawer>
        </Hidden>
        <Hidden smDown>
          <Drawer
            classes={{
              paper: classes.drawerPaper
            }}
            variant="permanent"
            open
          >
            {drawer}
          </Drawer>
        </Hidden>
      </nav>

      <main className={classes.content}>
        <div className={classes.toolbar} />
        {children}
      </main>
    </div>
  );
};

const mapStateToProps = state => ({
  authUser: state.sessionState.authUser
});

export default connect(mapStateToProps)(Shell);

如您所见,Shell 组件使用 HOC 使用来自会话状态的 authUser 道具包装它。我不知道这是否是导致页面加载时出现问题的原因?

ProfileIcon 组件在用户登录时也不会立即加载。类似于我之前提到的kage 网站。我不明白为什么会这样。我觉得我的代码到处都是。

我的signin.js 页面如下所示:

import React from "react";
import Router from "next/router";

import Button from "@material-ui/core/Button";
import { AppWithAuthentication } from "../src/components/App";
import { auth, provider } from "../src/firebase/firebase";
import { db } from "../src/firebase";
import * as routes from "../src/constants/routes";

const SignInPage = () => (
  <AppWithAuthentication>
    <h1>Sign In</h1>
    <SignInForm />
  </AppWithAuthentication>
);

const updateByPropertyName = (propertyName, value) => () => ({
  [propertyName]: value
});

const INITIAL_STATE = {
  user: null,
  error: null
};

class SignInForm extends React.Component {
  constructor(props) {
    super(props);

    this.state = { ...INITIAL_STATE };

    if (auth.currentUser) {
      console.log(`already signed in`);
      Router.push(routes.HOME);
    }
  }

  componentDidMount() {
    auth.onAuthStateChanged(user => {
      if (user) {
        console.log(user);

        // add them to the db and then redirect
        db.doCreateUser(
          user.uid,
          user.email,
          user.displayName,
          user.photoURL,
          false
        )
          .then(() => {
            this.setState(() => ({ ...INITIAL_STATE }));
            Router.push(routes.HOME);
          })
          .catch(error => {
            this.setState(updateByPropertyName("error", error));
          });
      } else {
        console.log(`No active user found. User must log in`);
      }
    });
  }

  onClick = () => {
    auth.signInWithRedirect(provider);
  };

  render() {
    return (
      <Button variant="contained" color="primary" onClick={this.onClick}>
        Sign In with Google
      </Button>
    );
  }
}

export default SignInPage;

export { SignInForm };

AppWithAuthentication 看起来像这样:

import React from "react";
import { compose } from "recompose";

import withAuthentication from "../Session/withAuthentication";
import withAuthorisation from "../Session/withAuthorisation";

const App = ({ children }) => (
  <div className="app">
    {children}
  </div>
);

const AppWithAuthentication = compose(
  withAuthentication,
  withAuthorisation(false)
)(App);

const AppWithAuthorisation = compose(
  withAuthentication,
  withAuthorisation(true)
)(App);

export { AppWithAuthentication, AppWithAuthorisation };

因此,每当用户进入我的网页并尝试访问任何“仅经过身份验证”的路由时,他们将首先看到该路由的内容几秒钟,然后然后被重定向到登录页。我不希望发生这种情况,我也不明白为什么会发生这种情况。

如何解决这些问题?我完全被想法困住了。需要一双新的眼睛来帮助我了解问题所在。

【问题讨论】:

  • 有几种方法可以防止仅显示经过身份验证的内容,例如,您可以进行服务器端检查或使用显示其他内容的 HOC 组件,同时检查用户是否已登录。
  • @Nico 这是否意味着我的应用程序的当前行为是预期的?上面说的能解决我的问题吗?如果是这样,您能否链接一个有关如何实现上述目标的示例?
  • 服务器身份验证预计需要一秒钟。您可以在等待完成时显示加载屏幕。
  • 在初始页面加载期间,用户的身份验证状态为“待定”。您无法确认用户是否已登录,直到他们的状态得到解决并且任何auth.onAuthStateChanged() 处理程序都会收到通知。在这样的页面加载中,您应该在等待时渲染一个完整的页面加载 throbber,并且一旦您知道用户状态已确定,可能会预加载准备好渲染的组件。

标签: reactjs firebase firebase-authentication next.js google-authentication


【解决方案1】:

这是由身份验证进程的请求等待响应引起的。 使用开发人员工具 (Ctrl+Shift+I) -> 网络 -> 检查您在 chrome 中的网络连接,然后重新加载您的应用程序并查看请求是否触发。您会注意到,在您点击登录并选择帐户后,您所在的州将等待 https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAssertion?key=xxx 返回包含 Google 帐户名称等的响应。

使加载程序的可见性取决于请求返回的信息(在您的状态下创建一个变量(exp。:用户),一旦您从谷歌获得用户信息,将其设置为用户对象。然后您可以说!用户 - > 显示加载程序。您还可以使用额外的状态变量触发加载程序,当您单击登录时该变量设置为 true。

考虑到您正在使用 firebase,请查看 https://www.robinwieruch.de/complete-firebase-authentication-react-tutorial 了解更多信息。我也会在此处包含代码,但我认为您可以使用这些信息来根据您希望如何构建应用程序来构建我所说的内容。应该不是太大的交易。希望对您有所帮助!

【讨论】:

    【解决方案2】:

    删除这部分

    static async getInitialProps({ Component, ctx }) {
        return {
          pageProps: Component.getInitialProps
            ? await Component.getInitialProps(ctx)
            : {}
        };
      }
    
    

    让 nextjs 自己做优化。 当您自己处理初始道具时,优化丢失。

    【讨论】:

      猜你喜欢
      • 2021-10-13
      • 2020-08-23
      • 1970-01-01
      • 2020-11-29
      • 1970-01-01
      • 2017-11-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多