我们有一个 nextjs 13.5 网络应用程序。 Nextjs 13 使用新的应用程序路由器范例,其中所有组件(甚至客户端组件)都会在整个页面加载期间(例如刷新后)在服务器上进行首次渲染。 来自文档:
为了优化初始页面加载,Next.js 将使用 React 的 API 在服务器上为客户端和服务器组件呈现静态 HTML 预览。
在我们的应用程序中,登录流程是登录到提供令牌的外部服务。然后,该令牌用于验证对不同外部服务发出的其他请求 (graphql)。
我可以使用反应上下文/提供者范例将此令牌存储在内存中。
src/app/login/page.tsx
'use client';
import { useState, useCallback, useContext } from 'react';
import { AuthDispatchContext } from '@/contexts';
import { ActionNames } from '@/reducers';
export default function LoginPage() {
const loginDispatch = useContext(AuthDispatchContext);
const handleLogin = useCallback((token: string) => {
loginDispatch({payload: token, type: ActionNames.LOGIN})
}, []);
return (
<main>
<button onClick={() => handleLogin('a token we got from external service') > Login</button>
</main>
);
}
此上下文是在应用布局级别提供的。
src/app/layout.tsx
import React from 'react';
import { ApolloWrapper} from '@/lib/apollo-wrapper';
import AuthWrapper from '@/lib/auth-wrapper';
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<AuthWrapper>
<ApolloWrapper>
{children}
</ApolloWrapper>
</AuthWrapper>
</body>
</html>
);
}
登录后,其他组件(应用程序根布局的子组件)能够访问更新后的令牌,或者通过注销等清除令牌。特别是,ApolloWrapper 是一个创建需要访问令牌的 Apollo graphql 客户端的组件,它通过提供的上下文获取:
src/lib/apollo-wrapper.tsx
'use client';
import React, { useContext, useEffect } from 'react';
import { AuthContext } from '@/contexts';
import {
ApolloLink,
HttpLink,
SuspenseCache,
} from '@apollo/client';
import {
ApolloNextAppProvider,
NextSSRInMemoryCache,
SSRMultipartLink,
NextSSRApolloClient as ApolloClient,
} from '@apollo/experimental-nextjs-app-support/ssr';
import { AuthContextType } from '@/types';
import {onError} from 'apollo-link-error';
import { AuthAction, ActionNames } from '@/reducers';
function makeClient(authToken: AuthContextType) {
return function() {
const httpLink = new HttpLink({
uri: GRAPHQL_ENDPOINT,
headers: {
Authorization: `Bearer ${authToken}`
},
});
return new ApolloClient({
cache: new NextSSRInMemoryCache(),
link:
typeof window === 'undefined'
? ApolloLink.from([
new SSRMultipartLink({
stripDefer: true,
}),
logoutLink.concat(httpLink),
])
: logoutLink.concat(httpLink),
});
};
}
function makeSuspenseCache() {
return new SuspenseCache();
}
export function ApolloWrapper({ children }: React.PropsWithChildren) {
const authToken = useContext(AuthContext)
return (
<ApolloNextAppProvider
makeClient={makeClient(authToken)}
makeSuspenseCache={makeSuspenseCache}
>
{children}
</ApolloNextAppProvider>
);
}
这一切都很好……直到刷新。刷新后,显然上下文和提供程序被清除为其默认状态(在本例中为 null)。
刷新时,我需要客户端不需要登录。我选择的机制是localStorage:
src/lib/auth-wrapper.tsx
'use client';
import { reducer, ActionNames } from '@/reducers';
import { useReducer, useEffect } from 'react';
import { AuthContext, AuthDispatchContext } from '@/contexts';
export default function AuthWrapper({ children }: React.PropsWithChildren) {
const [authToken, dispatch] = useReducer(reducer, null);
// Check if user is already logged in from a previous reload
useEffect(() => {
if (localStorage.getItem('authToken') !== ''){
dispatch({
type: ActionNames.LOGIN,
payload: localStorage.getItem('authToken')
});
}
}, []);
return (
<AuthContext.Provider value={authToken}>
<AuthDispatchContext.Provider value={dispatch}>
{children}
</AuthDispatchContext.Provider>
</AuthContext.Provider>
);
}
这可以工作,但在该组件的实际第一次渲染时,localStorage 不可用,因为第一次渲染发生在服务器端。因此,调用必须存储在 useEffect 中。因此,在可以从 localStorage 获取令牌并将其设置为所有组件使用之前,整个组件树都会获得一次完整渲染。因此,该组件呈现:
/src/app/achildcomponent/page.tsx
'use client';
import { useContext, useEffect } from 'react';
import {
getMeQuery,
getMeResponse,
} from '@/lib/queries';
import { useSuspenseQuery } from '@apollo/experimental-nextjs-app-support/ssr';
import { AuthContext } from '@/contexts';
export default function Landing() {
// The following query attempts to run from the server, and thus fails
const { data, error } = useSuspenseQuery<getMeResponse>(getMeQuery);
return (
<main>
{error ? (
<p>Error {error.cause as string}</p>
) : !data ? (
<p>Loading...</p>
) : data ? (
<div>
<ul>
<li>{data.me.id}</li>
</ul>
</div>
) : null
}
</main>
);
}
它尝试从客户端进行 graphql 查询减去令牌。
我的第一个想法:没问题,重定向到没有身份验证令牌的登录。但是等等...有一个身份验证令牌,我不希望用户重新登录,这意味着,哦不,我必须检查是否没有身份验证令牌,因为我们没有登录,或者是因为应用程序处于第一个服务器端渲染模式。
第二个想法:至少可以,只需使 graphql 依赖组件和登录组件成为同级组件,以及 authcontext 的两个子组件,并且如果 authtoken 不可用,则不显示 graphql 组件。我走这条路线,但因为我的产品要求是让第一个根页面成为经过身份验证的页面,并在
/login
登录路线,如果未登录,则自动重定向设置为 /login
,我的代码开始变得草率超级快,我有一种直觉,我正走在一条糟糕的道路上。
所以现在我在这里问:我错过了什么?这肯定是一个已解决的问题,我只是对新的 App Router 范式不够熟悉,无法理解。
在 nextjs 13 App Router 应用程序中存储客户端组件获取的令牌的最佳机制是什么?
不要对应用程序路由器感到困惑。
token
用于识别客户端,因此这里仍然有客户端-服务器架构。
您可以将令牌存储在 cookie 中,该令牌将自动附加到对与 cookie 关联的同一域和路径发出的每个 HTTP 请求。您可以指定其他属性,例如过期时间、安全标志以及是否应通过 JavaScript 访问(HttpOnly 标志)。
现在令牌将附加到每个请求。您可以编写中间件来检查是否有有效令牌或不处理请求。
您可以做的是以下
因此您无需担心客户端组件中的令牌。只需向您的 NextJS 应用程序发出外部服务请求即可。 这是我现在自己的解决方案。