我对所有 next.js graphQL 世界都很陌生。
我刚刚找到了 useSWR,我想知道我是否可以将它与 Apollo-client 一起使用,而不是与 graphql-request 一起使用。
我对所有 next.js graphQL 世界都很陌生。
我刚刚找到了 useSWR,我想知道我是否可以将它与 Apollo-client 一起使用,而不是与 graphql-request 一起使用。
是的,你可以,我也可以。我有两个无头 CMS 并存。一种是通过 OneGraph 与 google 捆绑的 headless wordpress;另一个是 Headless Booksy,它是一个封闭源代码的 CMS,没有可公开访问的端点——在用户驱动的事件期间剖析网络选项卡以确定任何所需的标头/参数,并最终对其身份验证流程进行逆向工程,以使用异步分区在我的存储库中自动化它。
也就是说,是的,我同时使用了 apollo Client 和 SWR。这是 _app.tsx 配置
import '@/styles/index.css';
import '@/styles/chrome-bug.css';
import 'keen-slider/keen-slider.min.css';
import { AppProps, NextWebVitalsMetric } from 'next/app';
import { useRouter } from 'next/router';
import { ApolloProvider } from '@apollo/client';
import { useEffect, FC } from 'react';
import { useApollo } from '@/lib/apollo';
import * as gtag from '@/lib/analytics';
import { MediaContextProvider } from '@/lib/artsy-fresnel';
import { Head } from '@/components/Head';
import { GTagPageview } from '@/types/analytics';
import { ManagedGlobalContext } from '@/components/Context';
import { SWRConfig } from 'swr';
import { Provider as NextAuthProvider } from 'next-auth/client';
import fetch from 'isomorphic-unfetch';
import { fetcher, fetcherGallery } from '@/lib/swr-fetcher';
import { Configuration, Fetcher } from 'swr/dist/types';
type T = typeof fetcher | typeof fetcherGallery;
interface Combined extends Fetcher<T> {}
const Noop: FC = ({ children }) => <>{children}</>;
export default function NextApp({
Component,
pageProps
}: AppProps) {
const apolloClient = useApollo(pageProps);
const LayoutNoop = (Component as any).LayoutNoop || Noop;
const router = useRouter();
useEffect(() => {
document.body.classList?.remove('loading');
}, []);
useEffect(() => {
const handleRouteChange = (url: GTagPageview) => {
gtag.pageview(url);
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
return (
<>
<SWRConfig
value={{
errorRetryCount: 5,
refreshInterval: 43200 * 10,
onLoadingSlow: (
key: string,
config: Readonly<
Required<Configuration<any, any, Combined>>
>
) => [key, { ...config }]
}}
>
<ApolloProvider client={apolloClient}>
<NextAuthProvider session={pageProps.session}>
<ManagedGlobalContext>
<MediaContextProvider>
<Head />
<LayoutNoop pageProps={pageProps}>
<Component {...pageProps} />
</LayoutNoop>
</MediaContextProvider>
</ManagedGlobalContext>
</NextAuthProvider>
</ApolloProvider>
</SWRConfig>
</>
);
}
然后,我通过 index.tsx 中的 SWR 有这个 api-route 处理评论 + 这些评论的分页
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>
) {
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
}
);
const booksyReviews: BooksyReviewFetchResponse =
await response.json();
res.setHeader(
'Cache-Control',
'public, s-maxage=86400, stale-while-revalidate=43200'
);
return res.status(200).json(booksyReviews);
}
以及 index.tsx 中自定义选取框的以下 api 路由处理图像数据
import { NextApiRequest, NextApiResponse } from 'next';
import { Gallery } from '@/types/index';
import { getLatestBooksyPhotos } from '@/lib/booksy';
export default async function (
_req: NextApiRequest,
res: NextApiResponse<Gallery>
) {
const response: Response = await getLatestBooksyPhotos();
const booksyImages: Gallery = await response.json();
res.setHeader(
'Cache-Control',
'public, s-maxage=86400, stale-while-revalidate=43200'
);
return res.status(200).json(booksyImages);
}
现在,检查 index.tsx。为了清楚起见,我将代码分解为服务器端和客户端
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>
>
>;
}>
> {
console.log(ctx.params ?? '');
const apolloClient = initializeApollo();
const { data: DynamicSlugs } = await apolloClient.query<
DynamicNavQuery,
DynamicNavQueryVariables
>({
query: DynamicNavDocument,
variables: {
idHead: 'Header',
idTypeHead: WordpressMenuNodeIdTypeEnum.NAME,
idTypeFoot: WordpressMenuNodeIdTypeEnum.NAME,
idFoot: 'Footer'
}
});
const { data: LandingData } = await apolloClient.query<
LandingDataQuery,
LandingDataQueryVariables
>({
query: LandingDataDocument,
variables: {
other: WordPress.Services.Other,
popular: WordPress.Services.Popular,
path: Google.PlacesPath,
googleMapsKey: Google.MapsKey
}
});
const { other, popular, Places, businessHours, merchandise } =
LandingData;
const { Header, Footer } = DynamicSlugs;
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
? addApolloState(apolloClient, {
props: {
Header,
Footer,
other,
popular,
Places,
businessHours,
merchandise
},
revalidate: 600
})
: {
props: {
initialData,
initDataGallery
},
revalidate: 600
};
}
注意返回的 props 是如何作为 SWR vs Apollo Client 数据的函数来处理的?接下来很精彩
const dataGallery = await getLatestBooksyPhotos();
const initDataGallery: Gallery = await dataGallery.json();
const dataInit = await getLatestBooksyReviews({
reviewsPerPage: 10,
pageIndex: 1
});
const initialData: BooksyReviewFetchResponse =
await dataInit.json();
-- 它们来自 lib 目录。它们旨在在页面文件的服务器上使用,用于向 SWR 注入初始数据。本质上,它实现了与 api 路由文件相同的方式,但由于这些文件只能在客户端上使用,因此这是必要的解决方法。
现在为客户
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>
</>
);
}
登陆页面上有两个 useSWR 钩子:
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
);
在它们各自的提取器之后列出的initialData
和initDataGallery
值是从服务器传送到客户端并通过 推断的初始数据InferGetStaticPropsType<T>
。这为客户端数据获取时的第一个加载数据问题提供了解决方案。
使用 SWR 时,您可以进行的用于加快客户端上数据获取的附加配置是指定应预加载哪些 api 路由_document.tsx
及其对应的 fetcher(s) 的名称
<link
rel='preload'
href={`/api/booksy-fetch?reviews_page=1&reviews_per_page=10`}
as='fetcher'
crossOrigin='anonymous'
/>
<link
rel='preload'
href='/api/booksy-images'
as='fetcherGallery'
crossOrigin='anonymous'
/>
我在使用这两者时遇到了 0 个问题,我已经使用了大约一个月了,实际上将 SWR 合并到增强的DX/UX 和下一个分析中反映了这一点(它将 FCP 时间减少了 50% 以上至 0.4 秒, LCP 到 0.8 秒左右)。