如何为应该为 CI/CD 呈现服务器端的 ApolloClient 设置环境变量

How to set environment variable for ApolloClient that should be server side rendered for CI/CD

我有以下 apolloClient

/**
 * Initializes an ApolloClient instance. For configuration values refer to the following page
 * https://www.apollographql.com/docs/react/api/core/ApolloClient/#the-apolloclient-constructor
 *
 * @returns ApolloClient
 */
const createApolloClient = (authToken: string | null) => {
  const httpLinkHeaders = {
    ...(authToken && { Authorization: `Bearer ${authToken}` })
  };
  console.log('CREATING APOLLO CLIENT WITH HEADERS >>>>', httpLinkHeaders);

  console.log(
    'Graph Env Variable URL >>>>>',
    publicRuntimeConfig.GRAPHQL_URL
  );

  

  return new ApolloClient({
    name: 'client',
    ssrMode: typeof window === 'undefined',
    link: createHttpLink({
      uri: publicRuntimeConfig.GRAPHQL_URL,
      credentials: 'same-origin',
      headers: httpLinkHeaders
    }),
    cache: new InMemoryCache()
  });
};

/**
 * Initializes the apollo client with data restored from the cache for pages that fetch data
 * using either getStaticProps or getServerSideProps methods
 *
 * @param accessToken
 * @param initialState
 *
 * @returns ApolloClient
 */
export const initializeApollo = (
  accessToken: string,
  initialState = null,
  forceNewInstane = false
): ApolloClient<NormalizedCacheObject> => {
  // Regenerate client?
  if (forceNewInstane) {
    apolloClient = null;
  }

  const _apolloClient = apolloClient || createApolloClient(accessToken);

  // for pages that have Next.js data fetching methods that use Apollo Client,
  // the initial state gets hydrated here
  if (initialState) {
    // Get existing cache, loaded during client side data fetching
    const existingCache = _apolloClient.extract();

    // Restore the cache using the data passed from
    // getStaticProps/getServerSideProps combined with the existing cached data
    _apolloClient.cache.restore({ ...existingCache, ...initialState });
  }

  // For SSG and SSR always create a new Apollo Client
  if (typeof window === 'undefined') return _apolloClient;

  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient;
  return _apolloClient;
};

/**
 * Hook to initialize the apollo client only when state has changed.
 *
 * @param initialState
 *
 * @returns
 */
export const useApollo = (
  initialState: any
): ApolloClient<NormalizedCacheObject> => {
  return useMemo(() => {
    if (process.browser) {
      const accessToken = extractCookie(document.cookie, 'access_token');
      return initializeApollo(accessToken, initialState);
    }

    // document is not present and we can't retrieve the token but ApolloProvider requires to pass a client
    return initializeApollo(null, initialState);
  }, [initialState]);
};

像这样在_app.tsx文件中初始化

const updateApolloWithNewToken = useCallback(
    (accessToken: string) => {
      // Initialize apollo client with new access token
      setClient(
        initializeApollo(accessToken, pageProps.initialApolloState, true)
      );
      // Show the dashboard
      router.replace('/dashboard');
    },
    [router]
  );

有了下面的Next Config

const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');

module.exports = (phase, { defaultConfig }) => {
  console.log('Phase >>>>', phase);
  if (phase === PHASE_DEVELOPMENT_SERVER) {
    console.log('RETURNING DEVELOPMENT CONFIGURATION...');

    return {
      publicRuntimeConfig: {
        
        GRAPHQL_URL: process.env.GRAPHQL_URL
      }
    };
  }

  console.log('RETURNING PRODUCTION CONFIGURATION...');

  console.log('GRAPHQL_URL', process.env.GRAPHQL_URL);

  return {
    publicRuntimeConfig: {
      
      GRAPHQL_URL: process.env.GRAPHQL_URL
    }
  };
};

这是我的_app.tsx

function MyApp({ Component, pageProps }: AppProps) {
  // Grab the apollo client instance with state hydration from the pageProps
  const router = useRouter();
  const apolloClient = useApollo(pageProps.initialApolloState);
  const [client, setClient] = useState(apolloClient);

  
  React.useEffect(() => {
    // Refresh token on browser load or page regresh
    handleAcquireTokenSilent();

    // We also set up an interval of 5 mins to check if token needs to be refreshed
    const refreshTokenInterval = setInterval(() => {
      handleAcquireTokenSilent();
    }, REFRESH_TOKEN_SILENTLY_INTERVAL);

    return () => {
      clearInterval(refreshTokenInterval);
    };
  }, []);

  
  const updateApolloWithNewToken = useCallback(
    (accessToken: string) => {
      // Initialize apollo client with new access token
      setClient(
        initializeApollo(accessToken, pageProps.initialApolloState, true)
      );
      // Show the dashboard
      router.replace('/dashboard');
    },
    [router]
  );

  return pageProps.isAuthenticated || pageProps.shouldPageHandleUnAuthorize ? (
    <ApolloProvider client={client}>
      <ThemeProvider theme={theme}>
        <SCThemeProvider theme={theme}>
          <StylesProvider injectFirst>
            <Component
              {...pageProps}
              updateAuthToken={updateApolloWithNewToken}
            />
          </StylesProvider>
        </SCThemeProvider>
      </ThemeProvider>
    </ApolloProvider>
  ) : (
    <UnAuthorize />
  );
}

/**
 * Fetches the Me query so that pages/components can grab it from the cache in the
 * client.
 *
 * Note: This disables the ability to perform automatic static optimization, causing
 * every page in the app to be server-side rendered.
 */
MyApp.getInitialProps = async (appContext: AppContext) => {
  const appProps = await App.getInitialProps(appContext);
  const req = appContext.ctx.req;

  // Execute Me query and handle all scenarios including, unauthorized response, caching data so pages can grab
  // data from the cache
  return await handleMeQuery(appProps, req);
};

export default MyApp;

我的问题是,当我 运行 构建 yarn 时,出现服务器错误,生成 500 页。我知道这是因为在创建 Apollo Client 时它无法访问 publicRuntimeConfig,当我 运行 yarn build 时,Next 似乎正在尝试构建 ApolloClient,我正在使用 getInitialProps 和 getServerSideProps 所以我只想在 运行 时而不是在构建时访问所有环境变量,因为我们希望为我们的管道构建一个。

我的应用程序中使用 publicRuntimeConfig 的所有其他环境变量都在工作,我通过在构建时删除环境变量并在启动时重新添加它们进行测试,应用程序正常运行。

如果没有办法使用 apollo 客户端执行此操作,建议在应用程序启动时将不同的 uri 作为环境变量传递,而不是在为 Apollo 客户端构建时或替代解决方案?

提前感谢您的帮助

所以我不知道我是否把问题解释得足够好。

基本上 graphql URL 根据其在开发、暂存和生产中所处的环境而有所不同,但是它们应该使用相同的构建,因此我需要通过以下方式访问 GRAPHQL_URL 运行时间变量,但在我当前的设置中它只是未定义。

第一,代码冗余,效率低下。主要是 updateApolloWithNewTokenuseApollo 钩子,还有注入 accessToken.

的方式

我建议将 ApolloClient 放在它自己的单独文件中,并针对您的用例使用它的可配置 links

然而,真正的问题很可能在于客户端的初始化以及您对 memoizing 客户端的尝试。

首先,尝试更新以下内容,

    link: createHttpLink({
      uri: publicRuntimeConfig.GRAPHQL_URL,
      credentials: 'same-origin',
      headers: httpLinkHeaders
    }),

  link: createHttpLink({
    // ...your other stuff
    uri: () => getConfig().publicRuntimeConfig.GRAPHQL_URL
  })

如果这不能立即奏效,我建议尝试使用最外面的最小示例。

创建一个您导出的客户端、一个提供程序和一个使用它的组件(不使用状态、useEffect 或其他任何东西)。如果还是不行,我们可以从那里开始。

通常你 don't want to use publicRuntimeConfig 因为它增加了开销并且对你的用例来说是不必要的。它还会禁用自动静态优化。

传统上,环境变量是根据环境处理动态设置的方式。 Next.js 有三个 default environments – 开发、测试和生产。

您的 Next.js 应用程序将根据环境自动抓取(并合并)正确的变量。

.env.local

GRAPHQL_URL = localhost:8000/graphql

.env.test

GRAPHQL_URL = test.example.com/graphql

.env 或 .env.production

GRAPHQL_URL = production.example.com/graphql

Apollo 配置

new ApolloClient({
 link: createHttpLink({
  uri: process.env.GRAPHQL_URL,
 })
});

环境变量文件

在您的项目根目录中,您可以创建名为

的文件
  • .env
  • .env.local
  • .env.test
  • .env.test.local
  • .env.production

我相信您可以将 .local 附加到任何环境以创建仅限本地的版本 .env.production.local - 这在您的情况下用途有限


Next.js 环境变量

所有环境 - .env

  • 在开发、测试和生产环境中加载。此文件用于所有环境的默认值。如果另一个环境变量文件中有同名变量,则此文件中的变量将被覆盖。

Development environment .env.local

  • 仅在 NODE_ENV = development(下一个开发者)
  • 时加载

Test environment .env.test

  • 仅在NODE_ENV = test
  • 时加载

Test environment .env.test.local

  • 仅在NODE_ENV = test和本地
  • 时加载

生产环境.env.production

  • 仅在NODE_ENV = production(下次启动)时加载

示例生产变量

创建一个 .env.production 并在其中添加变量。您可以对测试变量和仅限局部变量重复相同的操作。


备注

将您的 .env 添加到 .gitignore 是一种很好的做法,这样您就不会不小心将秘密提交到您的存储库。您至少应从 git.

中省略 .env*.local 个文件

根据您的 CI/CD 设置,您可以在部署平台中设置环境变量,如 vercel、github 操作等。这将允许您在托管平台,而不是在您的代码中。

如果你需要一个环境变量 accessible in the browser 你需要在变量前加上 NEXT_PUBLIC_