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

Posted

技术标签:

【中文标题】如何为 ApolloClient 设置环境变量,该变量应该在 CI/CD 的服务器端呈现【英文标题】:How to set environment variable for ApolloClient that should be server side rendered for CI/CD 【发布时间】:2021-12-30 15:48:01 【问题描述】:

我有以下 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',
    s-s-rMode: 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 s-s-r 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]
  );

使用下面的下一个配置

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 build 时,我得到一个生成 500 页面的服务器错误。我知道这是因为在创建 Apollo Client 时它无法访问 publicRuntimeConfig,看起来 Next 在我运行 yarn build 时正在尝试构建 ApolloClient,我正在使用 getInitialProps 和 getServerSideProps 所以我只想访问所有运行时而不是构建时的 env 变量,因为我们想要为我们的管道构建一个版本。

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

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

感谢您提前提供的任何帮助

所以我不知道我是否已经充分解释了这个问题。

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

【问题讨论】:

【参考方案1】:

首先,存在冗余代码和低效率。主要是钩子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
  )

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

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

【讨论】:

【参考方案2】:

通常你 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

阿波罗配置

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_

【讨论】:

环境变量被注入,因此与运行时不兼容(它们无法更改)。该问题要求运行时(如果需要,我猜是替代解决方案)。如果您不关心运行时注入,我也建议您这样做。 这是一种比 next.js 中的运行时变量更标准的方法。 Example production only variable 部分专门回答“建议在应用程序启动时而不是在构建时将不同的 uri 作为 env 变量传递” @Anders Kitson 你能澄清一下你是否想要改变环境变量的能力或者只是想要拥有不同变量的能力。照原样,肖恩的回答会要求您每次更改环境时都重新构建。 我需要 GRAPHQL_URL 的变量在运行时在 azure 的应用程序设置中是可更改的,因为如果它是一个构建,我们只做一个构建,这将导致每个阶段的开发、登台和生产都有相同的网址。我会在你的回答中尝试你的策略@urmzd 谢谢并报告 每个阶段都有自己的变量(graphql url)定义在相应的环境变量文件中。您只运行一个构建,Next 会根据阶段自动获取正确的环境变量。您的根目录中将有 3 个 .env 文件 - 每个阶段一个(见帖子)。 Next.js 不推荐使用 publicRuntimeConfig 来处理环境变量。检查帖子中的第一个链接。

以上是关于如何为 ApolloClient 设置环境变量,该变量应该在 CI/CD 的服务器端呈现的主要内容,如果未能解决你的问题,请参考以下文章

如何为 [Setup] AppVersion 使用编译时环境变量?

如何为反应应用程序设置环境变量?

如何为 csh/tcsh 中的一个命令设置环境变量

如何为 OSX Mountain Lion 上的应用程序设置环境变量?

如何为 Vue Apollo 导入 Apollo Client 3?

如何为 rspec 设置 ENV 变量?