В предыдущем сообщении Apollo graphql client применялся для разработки чисто клиентского приложения. В документации Apollo graphql client есть (очень лаконичный) раздел, посвященный серверному рендерингу и изоморфным приложениям.
Одна из сложностей серверного рендеринга в react.js является необходимость асинхронной загрузки данных, т.к. серверный рендеринг в reaсt.js — операция синхронная. Например, фреймверк next.js предлагает для этого использовать специальный компонент page в котором реализован дополнительный метод static async getInitialProps()
, в котором предлагается проводить асинхронную загрузку данных. Такое решение не лишено недостатков. Например, этот метод статический, следовательно не имеет доступ к экземпляру компонента, метод реализован только для компонента самого верхнего уровня и отсутствует у вложенных компонентов. Решение с Apollo graphql client может быть использовано для компонента произвольного уровня вложенности.
Продолжим работу с проектом который рассматривался в предыдущем сообщении. В серверном коде есть два ключевых момента: использование в качестве корневого элемента компонент ApolloProvider client={client}
и асинхронное получение данных функцией await getDataFromTree(App)
. За кадром ApolloProvider определяет какие запросы нужно выполнить для рендеринга компонентов с учетом выбранного роута, выполняет эти запросы и передает в компоненты при рендеринге.
...
import AppRouter from './AppRouter';
import assets from '../build/asset-manifest.json';
module.exports = async (req, res, next) => {
const client = new ApolloClient({
ssrMode: true,
link: createHttpLink({
uri: 'https://api.graph.cool/simple/v1/ciyz901en4j590185wkmexyex',
headers: {
cookie: req.header('Cookie'),
},
fetch,
}),
cache: new InMemoryCache(),
});
const context = {};
const App = <ApolloProvider client={client}>
<StaticRouter location={req.url} context={context}>
<AppRouter />
</StaticRouter>
</ApolloProvider>;
await getDataFromTree(App)
const html = ReactDOMServer.renderToString((App));
const initialState = client.extract();
res.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Conduit</title>
<link rel="stylesheet" href="/${assets['main.css']}">
</head>
<body>
<script>
// WARNING: See the following for security issues around embedding JSON in HTML:
// http://redux.js.org/docs/recipes/ServerRendering.html#security-considerations
window.__APOLLO_STATE__ = ${JSON.stringify(initialState, null, 2).replace(/</g, '\u003c')};
</script>
<div id="app">${html}</div>
<script src="/${assets['main.js']}"></script>
</body>
</html>
`);
res.end();
}
Соответственно, для восстановления данных на клиенте необходимо восстановить состояние клиента передав параметр в конструктор клиента: cache: new InMemoryCache().restore(window.__APOLLO_STATE__)
.
import App from './App';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error';
import { ApolloLink } from 'apollo-link';
import { InMemoryCache } from "apollo-cache-inmemory";
const client = new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.map(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
),
);
if (networkError) console.log(`[Network error]: ${networkError}`);
}),
new HttpLink({
uri: 'https://api.graph.cool/simple/v1/ciyz901en4j590185wkmexyex',
// credentials: 'same-origin'
})
]),
cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
});
hydrate(<App client={client} />, document.getElementById('app'));
Код этого примера я поместил в ветку ssr репозитария.
Таким образом, реализация серверного рендеринга и универсальных приложений при помощи Apollo graphql client очень лаконична и мало отличается от реализации чисто клиентского рендеринга. Если API grqphql реализованы на том же сервере, где реализован SSR, есть возможность избежать запросов API через сеть (как это практически всегда делают универсальные приложения) а запрашивать нужные функции grqphql их непосредственным вызовом внутри node.js.
apapacy@gmail.com
20 мая 2018 года.
Автор: apapacy