在前一篇文章中,我们建立了一个 JWT JSON server,用来练习如何在 Next.js 中串接 JWT 验证,这个 JSON server 提供 POST /auth/login
的登入 API,以及 GET /products
与 GET /products/:id
两个能够取得产品资料的 API,而两个产品资料 API 都必须在 header 中带上 accessToken
,否则 JSON server 会回传 HTTP 401,阻挡使用者获得产品资讯。
JWT JSON server 的 repo: https://github.com/leochiu-a/fake-api-jwt-json-server
在 Next.js 中设定验证逻辑主要是放在 API routes 中,使用 NextAuth 建立客制化的验证流程,当使用者呼叫 NextAuth 提供的 signIn
时便会出发验证流程,验证成功後会得到一个 accessToken
,这个 accessToken
必须在打产品资料 API 时带上。
现在我们已经实作完了 API routes 的部分,接下来要继续实作在页面中的逻辑。我们接着要实作的逻辑很单纯,目标是使用者登入後可以成功浏览「产品列表页面」与「产品详细页面」,这两个页面在前面的章节中已经用了很多次,这次同样也是拿这两个页面来练习。
LoginForm 的样式 https://gist.github.com/leochiu-a/62d2e9dce4d1a8b09905f35ca8bf4a8a
首先,我们要来撰写一个登入的页面 pages/login/index.tsx
,在这个页面中会有一个 form 表单,表单中包含了使用者的 email
与 password
两个栏位,以及一个登入按钮,点击之後会触发 NextAuth 的验证流程,验证成功後转址到产品列表页面 /products
。
import { useState, SyntheticEvent } from "react";
import { useRouter } from "next/router";
import { signIn } from "next-auth/client";
import {
AuthSection,
Login,
ControlItem,
ControlLable,
ControlInput,
SubmitButtonWrapper,
SubmitButton,
} from "./index.style";
const LoginForm = () => {
// 使用者的状态与登入逻辑...
return (
<AuthSection>
<Login>Login</Login>
<form onSubmit={handleSubmit}>
<ControlItem>
<ControlLable htmlFor="email">Your Email</ControlLable>
<ControlInput
type="email"
id="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</ControlItem>
<ControlItem>
<ControlLable htmlFor="password">Your Password</ControlLable>
<ControlInput
type="password"
id="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</ControlItem>
<SubmitButtonWrapper>
<SubmitButton type="submit">Login</SubmitButton>
</SubmitButtonWrapper>
</form>
</AuthSection>
);
};
export default LoginForm;
在 <LoginForm />
中管理使用者的状态与登入的逻辑也很单纯,使用两个 useState
分别储存使用者的 email
与 password
,建立一个 onSubmit
的 callback function,在这个 function 中会呼叫 NextAuth
提供的 signIn()
,并指定是客制化的验证流程 credentials
,匹配的是 NextAuth 在 API routes 设定的 Providers.Credentials
。
在验证成功後,使用 useRouter()
转址到产品列表页面 /products
。
import { useState, SyntheticEvent } from "react";
import { useRouter } from "next/router";
import { signIn } from "next-auth/client";
import {
AuthSection,
Login,
ControlItem,
ControlLable,
ControlInput,
SubmitButtonWrapper,
SubmitButton,
} from "./index.style";
const LoginForm = () => {
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const router = useRouter();
const handleSubmit = async (event: SyntheticEvent) => {
event.preventDefault();
const result = await signIn("credentials", {
redirect: false,
email,
password,
});
if (result?.ok) {
router.push("/products");
}
};
// return component
};
export default LoginForm;
在这个页面中的逻辑与在前面章节中看到的大同小异,比较不一样的是在 getServerSideProps
中的逻辑。在使用者登入後,如果想要取得 accessToken
则可以使用 NextAuth 提供的 getSession()
,这个 function 必须带入由 getServerSideProps
的参数 ctx
,这样才能取得使用者的验证讯息。
然後在取得 accessToken
後,要在 fetch
的 headers
中设定 JWT 的验证讯息,否则打产品列表 API 服务器会回传 HTTP 401,禁止我们取得资料。最後,成功取得资料後,把 products
当作 props 传入到 component 中,现在应该可以顺利看到产品列表页面。
import { GetServerSidePropsContext } from "next";
import { getSession } from "next-auth/client";
import ProductCard from "../../components/ProductCard";
import { Product } from "../../fake-data";
import { PageTitle, ProductGallery } from "./index.style";
interface HomeProps {
products: Product[];
}
const Home = ({ products }: HomeProps) => {
return (
<>
<PageTitle>商品列表</PageTitle>
<ProductGallery>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</ProductGallery>
</>
);
};
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const session = await getSession(ctx);
const res = await fetch(`http://localhost:8000/products`, {
headers: {
Authorization: `Bearer ${session?.accessToken}`,
},
});
const products = await res.json();
return {
props: {
products,
session,
},
};
}
export default Home;
根据 NextAuth 的说明,我们可以在 /pages/__app.ts
中新增 context provider,这样做可以提升取得 session 的效率,例如从 useSession
的原始码中就可以看到,它会先尝试从 context 中取得 session,如果没有 context 再走类似 getSession
的流程,会自动打 /session
API 从服务器中取的验证资讯。
由此可知,没有 Provider
的设定是会在切换页面时,如果页面中刚好有 useSession
,会让页面多打很多次 /session
API,造成每次使用者都必须等待一段时间後才看得到内容。
import { AppProps } from "next/app";
import { Provider } from "next-auth/client";
function MyApp({ Component, pageProps }: AppProps) {
return (
<Provider session={pageProps.session}>
<Component {...pageProps} />
</Provider>
);
}
export default MyApp;
但是,实际上 Provider
有作用的前提是一个页面是必须是 SSR,也就是使用 Next.js 的 getServerSideProps
或 getInitialProps
,读者也可以从上述中看到 session 是透过 pageProps.session
取得,所以如果一个页面不是 SSR,就不能得到 context API 的帮助, useSession
还是会照常打 API。
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
return { session: await getSession(ctx) };
}
// 或者
Page.getInitialProps = async (ctx) => {
return { session: await getSession(ctx) };
};
最後,我们要来实作「产品详细页面」中的逻辑,在这个页面中我们使用的是 client-side rendering,并且使用到 useSWR
这个 API。
基本上逻辑与前面提到 CSR 的实作差不多,只差在我们会用到 useSession
取得使用者验证资讯,将 accessToken
带入到 fetch
的 header 中。
这边要特别注意的是,我们传入到 fetcher
中的数值变成是一个物件,这个物件包含 id
与 accessToken
,而 useSWR
触发 fetcher
的时机是看传入的第一个参数 key
是否改变,而如果每次渲染时 params
的记忆体位置都不同,将会导致 key
改变,最终导致 API 被无限次呼叫。
所以,为了解决这个问题要使用 useMemo
将 params
记忆起来,再重新渲染时不会造成 useSWR
传入的第一次参数记忆体位置改变。
import { useMemo } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { useSession } from "next-auth/client";
import useSWR from "swr";
import { Product as ProductType } from "../../fake-data";
import ProductCard from "../../components/ProductCard";
import { PageTitle, ProductContainer, BackLink } from "./[id].style";
type Params = {
id: string;
accessToken: string;
};
const fetcher = (url: string, { id, accessToken }: Params) => {
return fetch(`http://localhost:8000${url}/${id}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
}).then((res) => res.json());
};
const Product = () => {
const router = useRouter();
const { id } = router.query;
const [session, loading] = useSession();
const params = useMemo(
() => ({ id, accessToken: session?.accessToken }),
[id, session]
);
const { data: product, error } = useSWR<ProductType>(
id && !loading ? ["/products", params] : null,
fetcher
);
if (!product || error) return <div>loading</div>;
return (
<>
<PageTitle>商品详细页面</PageTitle>
<BackLink>
<Link href="/products">回产品列表</Link>
</BackLink>
<ProductContainer>
<ProductCard product={product} all />
</ProductContainer>
</>
);
};
export default Product;
前言 今天要来介绍 string Methods,可以把 string 进行各种处理,来做出你想做的...
本系列文章同步发布於笔者网站 前一篇文章以比较非技术角度介绍了 OpenStack 这个专案。今天开...
今天先建立专案,还有开始慢慢地定架构,装 dependencies ~ Setup 这专案想要用 N...
在某些应用来说,有时会看的到这些的工具,像是预约、或是创立帐号时要输入的生日等,都算是好用的一个小功...
“Slept, awoke, slept, awoke, miserable life.” ― f...