【发布时间】: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