首页 > 解决方案 > 从 nextjs 发出 API 请求是否正常?

问题描述

目前,我在 NodeJS (Express) 中有我的后端,并且目前正在从前端的 create-react-app 迁移到 NextJS。我应该将 API 请求从 NextJS 发送到 express 服务器,还是应该将 API 移动到 NextJS 本身(NextJS 直接与数据库交互)?在 NextJS 中编写 API 组件(与 DB 交互的逻辑)是否正常?或者我应该采用更传统的 NextJS 方法与我的旧快递服务器交互吗?

从长远来看,这两种方法中哪一种更好(我知道在 NextJS 中编写 API 组件在页面加载速度方面更快,但会高度耦合我的前端和后端逻辑):

  1. NextJS 与在单独的后端服务器(Express 服务器)中编写的 API 交互

  2. 将部分/全部后端逻辑移动到 NextJS,NextJS 直接与数据库交互

标签: node.jsreactjsdatabaseexpressnext.js

解决方案


您绝对应该使用无服务器节点 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);
}

首先,使用getLatestBooksyReviewspages/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 是一个全栈打孔的前端框架

您还可以查看他们的快速示例

和一个 mongodb 示例 (他们也有一个 mysql 示例)


推荐阅读