在 Next.js 中设置 Apollo GraphQL 并启用服务器端渲染。先决条件 为什么要 SEO? 应用程序设置 优势 投入生产 扩展此……

2025-06-08

使用服务器端渲染在 Next.js 中设置 Apollo GraphQL。

先决条件

为什么要进行 SEO?

应用程序设置

好处

投入生产

扩展这个..

单页应用程序是构建现代前端应用程序的流行方式。然而,客户端渲染的最大缺点是 SEO(搜索引擎优化)效果不佳。在本文中,我们将学习如何使用Next.js(一个 React 框架)搭建一个 React 应用,并使用来自 GraphQL API 的远程数据在服务器端渲染初始页面。

先决条件

  • Node.js ≥ 12.16.2 (LTS)
  • 反应
  • Next.js
  • GraphQL
  • Apollo 客户端
  • Yarn 包管理器

为什么要进行 SEO?

现在你可能会问,SEO 为什么重要?嗯……如果你正在构建一个仪表盘或一个在内网使用的应用程序,那么 React 服务器渲染和 SEO 在你的产品待办事项中可能并不重要。此外,如果你的公司从事电商行业,那么 SEO 就至关重要。SEO 可以确保你的产品列表或产品页面被 Google 和其他搜索引擎收录并排名靠前。这间接地会为潜在买家带来更多自然浏览量,从而极大地影响你的公司在线收入。😉

应用程序设置

搭建新的 Next.js 应用

首先创建一个新文件夹,并使用默认标志初始化 package.json 文件。我这里使用的是 yarn,但也可以使用 npm 安装和运行所有内容。

mkdir react-graphql-ssr
yarn init -y
Enter fullscreen mode Exit fullscreen mode

太棒了!现在我们已经初始化了一个新项目,是时候添加一些依赖项了。让我们安装下一个,react 和 react-dom。打开你最喜欢的终端并运行以下命令:

yarn add next react react-dom
Enter fullscreen mode Exit fullscreen mode

你的 package.json 现在应该看起来像这样:

{
    "name": "react-graphql-ssr",
    "version": "1.0.0",
    "main": "index.js",
    "license": "MIT",
    "author": "Angad Gupta",
    "dependencies": {
        "next": "^9.3.5",
        "react": "^16.13.1",
        "react-dom": "^16.13.1"
    }
}
Enter fullscreen mode Exit fullscreen mode

让我们添加一些脚本来运行应用程序。好消息是,与 create-react-app 类似,Next.js 抽象出了 web-pack 配置,并默认提供了 3 个脚本来帮助你开始开发,让你专注于产品本身,而不是底层的 web-pack 配置。

  • 带有热代码重新加载和好东西的开发脚本
  • 构建脚本以打包您的应用程序以供生产
  • 启动脚本以在生产中运行您的应用程序。
"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}
Enter fullscreen mode Exit fullscreen mode

你的 package.json 现在应该看起来像这样:

{
    "name": "react-graphql-ssr",
    "version": "1.0.0",
    "main": "index.js",
    "license": "MIT",
    "author": "Angad Gupta",
    "scripts": {
        "dev": "next",
        "build": "next build",
        "start": "next start"
    },
    "dependencies": {
        "next": "^9.3.5",
        "react": "^16.13.1",
        "react-dom": "^16.13.1"
    }
}
Enter fullscreen mode Exit fullscreen mode

呼……现在您已经在本地设置了应用程序,让我们创建一个 pages 目录并添加一个名为 index.js 的新页面。PS :您可以扩展此设置,修改 web-pack、babel,还可以添加 Typescript(如果您愿意),但本教程不强制要求这样做。

创建页面目录

mkdir pages
cd pages
touch index.js
Enter fullscreen mode Exit fullscreen mode

创建 React 组件

为 index.js 添加一个新的 react 组件

import React from 'react';

const IndexPage = () => {
    return (
        <>
            <h3>Setting up Apollo GraphQL in Next.js with Server Side Rendering</h3>
        </>
    );
};

export default IndexPage;
Enter fullscreen mode Exit fullscreen mode

现在,您应该能够在终端中使用yarn dev运行项目,并在http://localhost:3000上查看正在运行且启用了热代码重载的索引页。该页面将显示标题“在 Next.js 中使用服务器端渲染设置 Apollo GraphQL”。

添加 GraphQL

向项目添加 GraphQl 依赖项

yarn add graphql graphql-tag
Enter fullscreen mode Exit fullscreen mode

添加 Apollo 客户端

将 Apollo 客户端依赖项添加到项目中

yarn add @apollo/react-hooks @apollo/react-ssr apollo-cache-inmemory apollo-client apollo-link-http isomorphic-unfetch prop-types
Enter fullscreen mode Exit fullscreen mode

设置 Apollo 客户端

为了使 Apollo 客户端正常运行,在项目根文件夹中创建一个 libs 文件夹并添加一个 apollo.js 文件。

mkdir libs
cd libs
touch apollo.js
Enter fullscreen mode Exit fullscreen mode

将以下代码添加到 apollo.js 文件:

import React from 'react';
import App from 'next/app';
import Head from 'next/head';
import { ApolloProvider } from '@apollo/react-hooks';
import createApolloClient from '../apolloClient';

// On the client, we store the Apollo Client in the following variable.
// This prevents the client from reinitializing between page transitions.
let globalApolloClient = null;

/**
 * Installs the Apollo Client on NextPageContext
 * or NextAppContext. Useful if you want to use apolloClient
 * inside getStaticProps, getStaticPaths or getServerSideProps
 * @param {NextPageContext | NextAppContext} ctx
 */
export const initOnContext = (ctx) => {
    const inAppContext = Boolean(ctx.ctx);

    // We consider installing `withApollo({ ssr: true })` on global App level
    // as antipattern since it disables project wide Automatic Static Optimization.
    if (process.env.NODE_ENV === 'development') {
        if (inAppContext) {
            console.warn(
                'Warning: You have opted-out of Automatic Static Optimization due to `withApollo` in `pages/_app`.\n' +
                    'Read more: https://err.sh/next.js/opt-out-auto-static-optimization\n'
            );
        }
    }

    // Initialize ApolloClient if not already done
    const apolloClient =
        ctx.apolloClient ||
        initApolloClient(ctx.apolloState || {}, inAppContext ? ctx.ctx : ctx);

    // We send the Apollo Client as a prop to the component to avoid calling initApollo() twice in the server.
    // Otherwise, the component would have to call initApollo() again but this
    // time without the context. Once that happens, the following code will make sure we send
    // the prop as `null` to the browser.
    apolloClient.toJSON = () => null;

    // Add apolloClient to NextPageContext & NextAppContext.
    // This allows us to consume the apolloClient inside our
    // custom `getInitialProps({ apolloClient })`.
    ctx.apolloClient = apolloClient;
    if (inAppContext) {
        ctx.ctx.apolloClient = apolloClient;
    }

    return ctx;
};

/**
 * Always creates a new apollo client on the server
 * Creates or reuses apollo client in the browser.
 * @param  {NormalizedCacheObject} initialState
 * @param  {NextPageContext} ctx
 */
const initApolloClient = (initialState, ctx) => {
    // Make sure to create a new client for every server-side request so that data
    // isn't shared between connections (which would be bad)
    if (typeof window === 'undefined') {
        return createApolloClient(initialState, ctx);
    }

    // Reuse client on the client-side
    if (!globalApolloClient) {
        globalApolloClient = createApolloClient(initialState, ctx);
    }

    return globalApolloClient;
};

/**
 * Creates a withApollo HOC
 * that provides the apolloContext
 * to a next.js Page or AppTree.
 * @param  {Object} withApolloOptions
 * @param  {Boolean} [withApolloOptions.ssr=false]
 * @returns {(PageComponent: ReactNode) => ReactNode}
 */
export const withApollo = ({ ssr = false } = {}) => (PageComponent) => {
    const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
        let client;
        if (apolloClient) {
            // Happens on: getDataFromTree & next.js ssr
            client = apolloClient;
        } else {
            // Happens on: next.js csr
            client = initApolloClient(apolloState, undefined);
        }

        return (
            <ApolloProvider client={client}>
                <PageComponent {...pageProps} />
            </ApolloProvider>
        );
    };

    // Set the correct displayName in development
    if (process.env.NODE_ENV !== 'production') {
        const displayName =
            PageComponent.displayName || PageComponent.name || 'Component';
        WithApollo.displayName = `withApollo(${displayName})`;
    }

    if (ssr || PageComponent.getInitialProps) {
        WithApollo.getInitialProps = async (ctx) => {
            const inAppContext = Boolean(ctx.ctx);
            const { apolloClient } = initOnContext(ctx);

            // Run wrapped getInitialProps methods
            let pageProps = {};
            if (PageComponent.getInitialProps) {
                pageProps = await PageComponent.getInitialProps(ctx);
            } else if (inAppContext) {
                pageProps = await App.getInitialProps(ctx);
            }

            // Only on the server:
            if (typeof window === 'undefined') {
                const { AppTree } = ctx;
                // When redirecting, the response is finished.
                // No point in continuing to render
                if (ctx.res && ctx.res.finished) {
                    return pageProps;
                }

                // Only if dataFromTree is enabled
                if (ssr && AppTree) {
                    try {
                        // Import `@apollo/react-ssr` dynamically.
                        // We don't want to have this in our client bundle.
                        const { getDataFromTree } = await import('@apollo/react-ssr');

                        // Since AppComponents and PageComponents have different context types
                        // we need to modify their props a little.
                        let props;
                        if (inAppContext) {
                            props = { ...pageProps, apolloClient };
                        } else {
                            props = { pageProps: { ...pageProps, apolloClient } };
                        }

                        // Take the Next.js AppTree, determine which queries are needed to render,
                        // and fetch them. This method can be pretty slow since it renders
                        // your entire AppTree once for every query. Check out apollo fragments
                        // if you want to reduce the number of rerenders.
                        // https://www.apollographql.com/docs/react/data/fragments/
                        await getDataFromTree(<AppTree {...props} />);
                    } catch (error) {
                        // Prevent Apollo Client GraphQL errors from crashing SSR.
                        // Handle them in components via the data.error prop:
                        // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
                        console.error('Error while running `getDataFromTree`', error);
                    }

                    // getDataFromTree does not call componentWillUnmount
                    // head side effect therefore need to be cleared manually
                    Head.rewind();
                }
            }

            return {
                ...pageProps,
                // Extract query data from the Apollo store
                apolloState: apolloClient.cache.extract(),
                // Provide the client for ssr. As soon as this payload
                // gets JSON.stringified it will remove itself.
                apolloClient: ctx.apolloClient,
            };
        };
    }

    return WithApollo;
};
Enter fullscreen mode Exit fullscreen mode

太棒了!我们快完成了,现在让我们初始化一个 Apollo 客户端,它将链接到 GraphQL 服务器或网关。在根文件夹中,创建一个名为 apolloClient.js 的新文件。

touch apolloClient.js
Enter fullscreen mode Exit fullscreen mode

将以下代码添加到 apolloClient.js 文件:

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import fetch from 'isomorphic-unfetch';

export default function createApolloClient(initialState, ctx) {
    // The `ctx` (NextPageContext) will only be present on the server.
    // use it to extract auth headers (ctx.req) or similar.
    return new ApolloClient({
        ssrMode: Boolean(ctx),
        link: new HttpLink({
            uri: 'https://rickandmortyapi.com/graphql', // Server URL (must be absolute)
            credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
            fetch,
        }),
        cache: new InMemoryCache().restore(initialState),
    });
}
Enter fullscreen mode Exit fullscreen mode

为了本教程的目的,我们将使用免费的 Rick and Morty GraphQL API,它会返回所有角色及其详细信息。

编写查询以从 Rick and Morty GraphQL API 中获取所有角色

创建一个名为 gql 的文件夹,并创建一个名为 allCharacters.js 的新文件。
将以下查询添加到 allCharacters.js 文件。

mkdir gql
cd gql
touch allCharacters.js
Enter fullscreen mode Exit fullscreen mode
import gql from 'graphql-tag';

export const ALL_CHARACTERS = gql`
    query allCharacters {
        characters {
            results {
                id
                name
            }
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

该文件从我们之前安装的名为 graphql-tag 的 Node 模块导入了 gql 。gql 模板文字标签可用于简洁地编写 GraphQL 查询,并将其解析为标准的 GraphQL AST。这是将查询传递给 Apollo 客户端的推荐方法。

使用我们的索引页调用 GraphQL API

让我们在索引页中添加更多导入内容。

import { withApollo } from '../libs/apollo';
import { useQuery } from '@apollo/react-hooks';
import { ALL_CHARACTERS } from '../gql/allCharacters';
Enter fullscreen mode Exit fullscreen mode

我们正在从刚刚设置的 libs 文件夹导入 apollo 设置。

使用 apollo react-hooks 库中的 useQuery hook,并解析我们在 allCharacters.js 文件中编写的自定义查询

import React from 'react';
import { withApollo } from '../libs/apollo';
import { useQuery } from '@apollo/react-hooks';
import { ALL_CHARACTERS } from '../gql/allCharacters';

const IndexPage = () => {
    const { loading, error, data } = useQuery(ALL_CHARACTERS);
    if (error) return <h1>Error</h1>;
    if (loading) return <h1>Loading...</h1>;

    return (
        <>
            <h1>
                <h3>Setting up Apollo GraphQL in Next.js with Server Side Rendering</h3>
            </h1>
            <div>
                {data.characters.results.map((data) => (
                    <ul key={data.id}>
                        <li>{data.name}</li>
                    </ul>
                ))}
            </div>
        </>
    );
};

export default withApollo({ ssr: true })(IndexPage);
Enter fullscreen mode Exit fullscreen mode

Apollo useQuery 钩子接收 3 个对象。loading、error 和 data,它们管理 API 调用,如果没有错误则设置数据的 State。

一旦数据返回且没有任何错误,我们就可以使用本机 javascript map 函数映射数据,并创建一个以角色名称作为列表项的无序列表。

{
    data.characters.results.map((data) => (
        <ul key={data.id}>
            <li>{data.name}</li>
        </ul>
    ));
}
Enter fullscreen mode Exit fullscreen mode

我们现在导出 IndexPage,并将 ssr 标志设置为 true,服务器在后台渲染页面,并将最终渲染的版本与远程数据一起发送给客户端。

测试页面内容

让我们测试一下查看页面源代码时页面内容是否可用。在 Chrome 中右键单击主页,然后点击“查看页面源代码”。字符详细信息应该是页面标记的一部分。

您还可以在导出页面并测试时将 ssr 标志设置为 false。此外,根据您的网速,您可能会看到“正在加载...”文本(指示加载状态),然后最终看到获取的远程数据。

当检查和查看将 ssr 标志设置为 false 的页面源时,您会注意到返回的字符数据不再是我们标记的一部分,因为它现在是客户端呈现的。

好处

您可以根据业务需求,选择按页面进行客户端渲染或服务器端渲染。对于数据不断变化的页面(例如仪表板),客户端渲染是更佳选择。然而,对于不频繁变化且无需远程数据阻止的营销页面,可以提前发布预渲染或静态生成的页面,并将其缓存在 AWS 的 Cloud-front 等全球 CDN 上。

投入生产

在将此类设置投入生产之前,请务必使用 next/head 包优化页面 SEO,该包会公开 title 和 head 等 HTML 元素。请与您的团队合作,添加与您的业务相关的有意义的信息。

扩展这个..

您可以随意扩展本教程,添加更多功能、添加您喜欢的 UI 样式库,或者尝试使用嵌套查询或 GraphQL 参数。您可以通过 GitHub克隆并 fork 此仓库。

鏂囩珷鏉ユ簮锛�https://dev.to/angad777/setting-up-apollo-graphql-in-next-js-with-server-side-rendering-45l5
PREV
后端开发:2024 年的定义、统计数据和趋势
NEXT
节流和去抖动 - 解释