如何为应该为 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 运行时间变量,但在我当前的设置中它只是未定义。
第一,代码冗余,效率低下。主要是 updateApolloWithNewToken
和 useApollo
钩子,还有注入 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_
我有以下 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 运行时间变量,但在我当前的设置中它只是未定义。
第一,代码冗余,效率低下。主要是 updateApolloWithNewToken
和 useApollo
钩子,还有注入 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_