【问题标题】:Is it normal to do an API request from nextjs?从 nextjs 发出 API 请求是否正常?
【发布时间】:2021-05-25 09:55:45
【问题描述】:

目前,我在 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.js reactjs database express next.js


    【解决方案1】:

    您绝对应该使用无服务器节点 API 路由。使用 nextjs 使用数据库是正常的。 pages/api/** 中的无服务器 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)
        );
    };
    
    

    然后您可以在无服务器页面/api 路由中使用 getAccessToken 函数以用于客户端获取

    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);
    }
    
    

    首先,在 pages/index.tsx 的 getStaticProps(服务器端)中使用 getLatestBooksyReviews 函数(参见 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 在服务器端返回的道具通过默认导出在客户端上无缝使用,它作为初始数据注入,以便在首次加载时立即在生产中显示内容。然后,客户端在重新验证时过时 (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 是一个全栈打孔的前端框架

    you can also check out their express example

    and a mongodb example(they also have a mysql example)

    【讨论】:

    • 好的。但是在 getServerSideProps 中进行 fetch 调用的缺点是什么?假设运行 NextJS 的服务器的网络带宽很快,那应该没什么关系吧?
    • 不,不应该,但我认为在可以避免使用 getServerSideProps 时使用增量静态再生是可取的。 ISR(带有 revalidate 的 getStaticProps)是两全其美的,并增强了 SEO。使用 getServerSideProps 是类似的,类型推断的唯一区别是您必须在 Promise 包装器中将函数绑定泛型 P 传递到预期的返回类型(props: P,props 在这里是任意的)。然后你可以使用完全相同的推理。服务器端非常适合快速检查 cookie 等
    【解决方案2】:

    我认为你不需要将你的后端转换成nextjs,使用API​​请求,你会没事的吗?请记住构建单独后端的目的...

    【讨论】:

      猜你喜欢
      • 2020-05-21
      • 1970-01-01
      • 1970-01-01
      • 2021-02-12
      • 1970-01-01
      • 1970-01-01
      • 2019-05-02
      • 2014-01-06
      • 1970-01-01
      相关资源
      最近更新 更多