node.js - 从 nextjs 发出 API 请求是否正常?
问题描述
目前,我在 NodeJS (Express) 中有我的后端,并且目前正在从前端的 create-react-app 迁移到 NextJS。我应该将 API 请求从 NextJS 发送到 express 服务器,还是应该将 API 移动到 NextJS 本身(NextJS 直接与数据库交互)?在 NextJS 中编写 API 组件(与 DB 交互的逻辑)是否正常?或者我应该采用更传统的 NextJS 方法与我的旧快递服务器交互吗?
从长远来看,这两种方法中哪一种更好(我知道在 NextJS 中编写 API 组件在页面加载速度方面更快,但会高度耦合我的前端和后端逻辑):
NextJS 与在单独的后端服务器(Express 服务器)中编写的 API 交互
将部分/全部后端逻辑移动到 NextJS,NextJS 直接与数据库交互
解决方案
您绝对应该使用无服务器节点 API 路由。使用 nextjs 使用数据库是正常的。无服务器 api 路由pages/api/**
与 ./utils 或 ./lib 中的服务器端函数(命名这些目录的任意偏好)相结合,是分区逻辑的绝佳组合。例如,考虑我正在为一个使用 booksy 作为 CMS 的客户处理的项目中的文件中的以下内容。我不得不对他们的网络身份验证流和 api 路由进行逆向工程,以持久地远程提供数据。
import type { BooksyAuthResponse } from '@/types/index';
import { format } from 'date-fns';
import { parseUrl } from './helpers';
const EMAIL = process.env.BOOKSY_BIZ_EMAIL ?? "";
const PASSWORD = process.env.BOOKSY_BIZ_PASSWORD ?? "";
const API_KEY = process.env.NEXT_PUBLIC_BOOKSY_BIZ_API_KEY ?? "";
const FINGERPRINT =
process.env.NEXT_PUBLIC_BOOKSY_BIZ_X_FINGERPRINT ?? "";
export const getAccessToken =
async (): Promise<BooksyAuthResponse> => {
const response = await fetch(
`https://us.booksy.com/api/us/2/business_api/account/login?x-api-key=${API_KEY}&x-fingerprint=${FINGERPRINT}`,
{
method: 'POST',
headers: {
'X-Api-Key': API_KEY,
'X-Fingerprint': FINGERPRINT,
'Content-Type': 'application/json',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.152 Safari/537.36',
Connection: 'keep-alive',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br'
},
keepalive: true,
body: JSON.stringify({
email: EMAIL!,
password: PASSWORD!
})
}
);
const authData = await response.json();
return authData;
};
// consumed in getStaticProps of pages/index.tsx
export const getLatestBooksyReviews = async ({
reviewsPerPage,
pageIndex
}: BooksyPagination): Promise<Response> => {
const { access_token } = await getAccessToken();
pageIndex = 1;
reviewsPerPage = 10;
return fetch(
`https://us.booksy.com/api/us/2/business_api/me/businesses/481001/reviews/?reviews_page=${pageIndex}&reviews_per_page=${reviewsPerPage}`,
Object.freeze({
headers: {
'X-Api-key': API_KEY,
'X-Access-Token': `${access_token}`,
'X-fingerprint': FINGERPRINT,
Authorization: `s-G1-cvdAC4PrQ ${access_token}`!,
'Cache-Control':
'public, s-maxage=86400, stale-while-revalidate=43200',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.152 Safari/537.36',
Connection: 'keep-alive',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br'
},
method: 'GET',
keepalive: true
} as const)
);
};
然后,您可以getAccessToken
在无服务器页面/api 路由中使用该函数以用于客户端获取
import { NextApiRequest, NextApiResponse } from 'next';
import { BooksyReviewFetchResponse } from '@/types/booksy';
import fetch from 'isomorphic-unfetch';
import { getAccessToken } from '@/lib/booksy';
const API_KEY = process.env.NEXT_PUBLIC_BOOKSY_BIZ_API_KEY ?? '';
const FINGERPRINT =
process.env.NEXT_PUBLIC_BOOKSY_BIZ_X_FINGERPRINT ?? '';
export default async function (
req: NextApiRequest,
res: NextApiResponse<BooksyReviewFetchResponse>
) {
// console.log(req.headers);
const {
query: { reviews_page, reviews_per_page }
} = req;
const { access_token } = await getAccessToken();
const rev_page_number = reviews_page ? reviews_page : 1;
const reviews_pp = reviews_per_page ? reviews_per_page : 10;
const response = await fetch(
`https://us.booksy.com/api/us/2/business_api/me/businesses/481001/reviews/?reviews_page=${rev_page_number}&reviews_per_page=${reviews_pp}`,
{
headers: {
'X-Api-key': API_KEY,
'X-Access-Token': `${access_token}`,
'X-fingerprint': FINGERPRINT,
Authorization: `s-G1-cvdAC4PrQ ${access_token}`,
'Cache-Control':
's-maxage=86400, stale-while-revalidate=43200',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.152 Safari/537.36',
Connection: 'keep-alive',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br'
},
method: 'GET',
keepalive: true
}
);
// console.log(response.headers);
const booksyReviews: BooksyReviewFetchResponse =
await response.json();
res.setHeader(
'Cache-Control',
'public, s-maxage=86400, stale-while-revalidate=43200'
);
return res.status(200).json(booksyReviews);
}
首先,使用getLatestBooksyReviews
pages/index.tsx 中的 getStaticProps(服务器端)中的函数(请参阅 参考资料const initialData
)。这是将在默认导出中在客户端使用无服务器 booksy 功能的相同文件
export async function getStaticProps(
ctx: GetStaticPropsContext
): Promise<
GetStaticPropsResult<{
other: LandingDataQuery['other'];
popular: LandingDataQuery['popular'];
places: LandingDataQuery['Places'];
merchandise: LandingDataQuery['merchandise'];
businessHours: LandingDataQuery['businessHours'];
Header: DynamicNavQuery['Header'];
Footer: DynamicNavQuery['Footer'];
initDataGallery: Partial<
Configuration<Gallery, any, Fetcher<Gallery>>
>;
initialData: Partial<
Configuration<
BooksyReviewFetchResponse,
any,
Fetcher<BooksyReviewFetchResponse>
>
>;
}>
> {
const apolloClient = initializeApollo(
{ headers: ctx.params } ?? {}
);
await apolloClient.query<
DynamicNavQuery,
DynamicNavQueryVariables
>({
query: DynamicNavDocument,
variables: {
idHead: 'Header',
idTypeHead: WordpressMenuNodeIdTypeEnum.NAME,
idTypeFoot: WordpressMenuNodeIdTypeEnum.NAME,
idFoot: 'Footer'
}
});
await apolloClient.query<
LandingDataQuery,
LandingDataQueryVariables
>({
query: LandingDataDocument,
variables: {
other: WordPress.Services.Other,
popular: WordPress.Services.Popular,
path: Google.PlacesPath,
googleMapsKey: Google.MapsKey
}
});
const dataGallery = await getLatestBooksyPhotos();
const initDataGallery: Gallery = await dataGallery.json();
const dataInit = await getLatestBooksyReviews({
reviewsPerPage: 10,
pageIndex: 1
});
const initialData: BooksyReviewFetchResponse =
await dataInit.json();
return addApolloState(apolloClient, {
props: { initialData, initDataGallery },
revalidate: 600
});
}
现在,getStaticProps 在服务器端返回的 props 通过默认导出在客户端无缝使用,它作为初始数据注入,以便在首次加载时立即在生产中显示内容。然后,客户端在重新验证时过时 (SWR) 挂钩从无服务器功能中获取额外的数据进行分页。这创建了一个无缝的用户体验。它缓存数据,提供近乎即时的体验
const { data } =
useSWR<BooksyReviewFetchResponse>(
() =>
`/api/booksy-fetch?reviews_page=${reviews_page}&reviews_per_page=${reviews_per_page}`,
fetcher,
initialData
);
export default function Index<T extends typeof getStaticProps>({
other,
popular,
Header,
Footer,
merchandise,
places,
businessHours,
initialData,
initDataGallery
}: InferGetStaticPropsType<T>) {
const GalleryImageLoader = ({
src,
width,
quality
}: ImageLoaderProps) => {
return `${src}?w=${width}&q=${quality || 75}`;
};
const reviews_per_page = 10;
const [reviews_page, set_reviews_page] = useState<number>(1);
const page = useRef<number>(reviews_page);
const { data } =
useSWR<BooksyReviewFetchResponse>(
() =>
`/api/booksy-fetch?reviews_page=${reviews_page}&reviews_per_page=${reviews_per_page}`,
fetcher,
initialData
);
const { data: galleryData } = useSWR<Gallery>(
'/api/booksy-images',
fetcherGallery,
initDataGallery
);
// total items
const reviewCount = data?.reviews_count ?? reviews_per_page;
// total pages
const totalPages =
(reviewCount / reviews_per_page) % reviews_per_page === 0
? reviewCount / reviews_per_page
: Math.ceil(reviewCount / reviews_per_page);
// correcting for array indeces starting at 0, not 1
const currentRangeCorrection =
reviews_per_page * page.current - (reviews_per_page - 1);
// current page range end item
const currentRangeEnd =
currentRangeCorrection + reviews_per_page - 1 <= reviewCount
? currentRangeCorrection + reviews_per_page - 1
: currentRangeCorrection +
reviews_per_page -
(reviewCount % reviews_per_page);
// current page range start item
const currentRangeStart =
page.current === 1
? page.current
: reviews_per_page * page.current - (reviews_per_page - 1);
const pages = [];
for (let i = 0; i <= reviews_page; i++) {
pages.push(
data?.reviews ? (
<BooksyReviews pageIndex={i} key={i} reviews={data.reviews}>
<nav aria-label='Pagination'>
<div className='hidden sm:block'>
<p className='text-sm text-gray-50'>
Showing{' '}
<span className='font-medium'>{`${currentRangeStart}`}</span>{' '}
to{' '}
<span className='font-medium'>{`${currentRangeEnd}`}</span>{' '}
of <span className='font-medium'>{reviewCount}</span>{' '}
reviews (page:{' '}
<span className='font-medium'>{page.current}</span> of{' '}
<span className='font-medium'>{totalPages}</span>)
</p>
</div>
<div className='flex-1 inline-flex justify-between sm:justify-center my-auto'>
<button
disabled={page.current - 1 === 0 ? true : false}
onClick={() => set_reviews_page(page.current - 1)}
className={cn('landing-page-pagination-btn', {
' cursor-not-allowed bg-redditSearch':
reviews_page - 1 === 0,
' cursor-pointer': reviews_page - 1 !== 0
})}
>
Previous
</button>
<button
disabled={page.current === totalPages ? true : false}
onClick={() => set_reviews_page(page.current + 1)}
className={cn('landing-page-pagination-btn', {
' cursor-not-allowed bg-redditSearch':
reviews_page === totalPages,
' cursor-pointer': reviews_page < totalPages
})}
>
Next
</button>
</div>
</nav>
</BooksyReviews>
) : (
<ReviewsSkeleton />
)
);
}
useEffect(() => {
(async function Update() {
return (await page.current) === reviews_page
? true
: set_reviews_page((page.current = reviews_page));
})();
}, [page.current, reviews_page]);
return (
<>
<AppLayout
title={'The Fade Room Inc.'}
Header={Header}
Footer={Footer}
>
{galleryData?.images ? (
<Grid>
{galleryData.images
.slice(6, 9)
.map((img, i) => {
<GalleryCard
key={img.image_id}
media={galleryData}
imgProps={{
loader: GalleryImageLoader,
width: i === 0 ? 1080 : 540,
height: i === 0 ? 1080 : 540
}}
/>;
})
.reverse()}
</Grid>
) : (
<LoadingSpinner />
)}
{galleryData?.images ? (
<Marquee variant='secondary'>
{galleryData.images
.slice(3, 6)
.map((img, j) => (
<GalleryCard
key={img.image_id}
media={galleryData}
variant='slim'
imgProps={{
loader: GalleryImageLoader,
width: j === 0 ? 320 : 320,
height: j === 0 ? 320 : 320
}}
/>
))
.reverse()}
</Marquee>
) : (
<LoadingSpinner />
)}
<LandingCoalesced
other={other}
popular={popular}
places={places}
businessHours={businessHours}
merchandise={merchandise}
>
{data?.reviews ? (
<>
<>{pages[page.current]}</>
<span className='hidden'>
{
pages[
page.current < totalPages
? page.current + 1
: page.current - 1
]
}
</span>
</>
) : (
<ReviewsSkeleton />
)}
</LandingCoalesced>
</AppLayout>
</>
);
}
Next.js 是一个全栈打孔的前端框架
推荐阅读
- mvel - 如果使用的上下文具有字母数字键,则 MVEL2 无法评估表达式
- r - 如何自动化将在 R 中具有可变输入的函数
- list - Flutter:将列表保存在本地
- powerbi - Power BI DAX:使用行对筛选器上下文计数不同的度量
- mlr3 - 金融时间序列的 mlr3 重采样扩展包
- tensorflow - 使用梯度带自动区分张量流并将其合并到 keras NN
- flutter - 有没有办法在状态类中调用状态类?
- python - 我在 cv2 人脸识别器中遇到错误 - 您需要多个样本来学习模型。在函数'cv::face::LBPH::train'中
- java - 如何确保具有无限流的 Flux 在给定时间内完成?
- python - 3603 Azure 数据工厂上的 Azure 函数中的用户配置问题错误