Next.js + Mehrere Apollo Clients & GraphQL-Quellen
Paul Grieselhuber
Next.js + GraphQL ist eine hervorragende Möglichkeit, sowohl kleine als auch große statische Websites zu erstellen, vor allem, wenn man die statischen HTML-Exporte von Next.js nutzt (siehe my post on WPGraphQL für eine Anleitung zu diesem Thema mit GraphQL).
Eine der Fragen, die viele Entwickler zu haben scheinen, und die eine erstaunliche Menge an Googeln, Recherchen und Tests erfordert, um sie zu klären, ist der Aufbau einer dieser Websites mit zwei GraphQL-Datenquellen.
Ein wenig Hintergrund & Standardwerk
Um den Rahmen für die Lösung abzustecken, hier der Grund, warum dies eine kleine Herausforderung darstellt. Das grundlegende Setup für einen Apollo-Client in Next.js source umfasst drei Dateien:
- Unsere Komponente /pages/_app.js
- /lib/init-apollo.js
- /lib/with-apollo-client.js
Werfen wir zunächst einen Blick auf _app.js
:
import App, { Container } from "next/app";
import React from "react";
import withApolloClient from "../lib/with-apollo-client";
import { ApolloProvider } from "react-apollo";
class MyApp extends App {
render() {
const { Component, pageProps, apolloClient } = this.props;
return (
<Container>
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
</Container>
);
}
}
export default withApolloClient(MyApp);
Das Wesentliche hier ist, dass wir withApolloClient
and wrap the rest of our app in that component, passing the apolloClient
importieren, das von props als Ergebnis mitkommt. Dadurch erhalten unsere untergeordneten Komponenten Zugriff auf die Daten, die von der GraphQL-Quelle stammen, mit der Apollo verbunden ist. Weiter geht's.
In init-apollo.js
erstellen wir den Apollo-Client und sagen ihm, wo er sich verbinden soll:
import { ApolloClient, InMemoryCache, HttpLink } from "apollo-boost";
import fetch from "isomorphic-unfetch";
let apolloClient = null;
// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
global.fetch = fetch;
}
function create(initialState) {
// Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
return new ApolloClient({
connectToDevTools: process.browser,
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
link: new HttpLink({
uri: "https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn", // Server URL (must be absolute)
credentials: "same-origin", // Additional fetch() options like `credentials` or `headers`
}),
cache: new InMemoryCache().restore(initialState || {}),
});
}
export default function initApollo(initialState) {
// 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 (!process.browser) {
return create(initialState);
}
// Reuse client on the client-side
if (!apolloClient) {
apolloClient = create(initialState);
}
return apolloClient;
}
Ziemlich klar, was hier vor sich geht, aber behalten Sie die Link-Zeile im Auge, wir werden darauf zurückkommen...
Und schließlich with-apollo-client.js
:
import React from "react";
import initApollo from "./init-apollo";
import Head from "next/head";
import { getDataFromTree } from "react-apollo";
export default (App) => {
return class Apollo extends React.Component {
static displayName = "withApollo(App)";
static async getInitialProps(ctx) {
const { Component, router } = ctx;
let appProps = {};
if (App.getInitialProps) {
appProps = await App.getInitialProps(ctx);
}
// Run all GraphQL queries in the component tree
// and extract the resulting data
const apollo = initApollo();
if (!process.browser) {
try {
// Run all GraphQL queries
await getDataFromTree(
<App
{...appProps}
Component={Component}
router={router}
apolloClient={apollo}
/>
);
} 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();
}
// Extract query data from the Apollo store
const apolloState = apollo.cache.extract();
return {
...appProps,
apolloState,
};
}
constructor(props) {
super(props);
this.apolloClient = initApollo(props.apolloState);
}
render() {
return <App {...this.props} apolloClient={this.apolloClient} />;
}
};
};
Hier geben wir zwei verschiedene Versionen unserer HOC-App-Komponente zurück (die später die untergeordneten Komponenten in _app.js
umhüllt): die erste ist für den SSR-Modus, die zweite für alles andere.
Apollo erwartet einen Client
Sie haben wahrscheinlich die implizite Annahme in der gesamten obigen Logik bemerkt, dass es genau einen Client gibt.
Das Problem ist, dass ich mehrere Datenquellen haben möchte, und ich möchte sie in dem Stil abfragen, den die Bibliotheken, die ich in meinem Projekt verwende, vorsehen. Und ich möchte nicht etwas zusammenhacken, das mir die Möglichkeit nimmt, von all den großartigen Funktionen von Apollo zu profitieren usw.
Eine Option, die mir in den Sinn kam, war schema stitching, aber das schien mir einfach zu viel Aufwand zu sein, nur um zwei oder mehr Clients abfragen zu können.
Nachdem ich viele Diskussionen, viele Recherchen und mehr Refactoring durchgeführt hatte, als ich zugeben möchte, wurde mir klar, dass es darauf ankommt, wovon man wirklich "mehrere" haben möchte.
Die Lösung
Wie lautet also die Antwort auf diese Frage? Möchte ich mehrere Clients? Mehrere Anbieter?
Nein, da liegen Sie völlig falsch. Und wahrscheinlich, weil ich Sie mit dem Titel in die Irre geführt habe. Oder vielleicht, weil die Angular Apollo-Bibliothek diese Option hat, und es ist in der Tat mit mehreren Clients getan.
Aber wir sind in der React-Welt, und die Antwort ist: mehrere Links in Ihrem einen Apollo-Client.
Um das zu erreichen, müssen wir glücklicherweise nur kleine Änderungen an zwei unserer drei Dateien vornehmen.
In init-apollo.js
:
import { ApolloLink } from "apollo-link";
import { ApolloClient, InMemoryCache, HttpLink } from "apollo-boost";
import fetch from "isomorphic-unfetch";
let apolloClient = null;
// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
global.fetch = fetch;
}
// Create First Link
const firstLink = new HttpLink({
uri: "https://www.firstsource.com/api",
headers: yourHeadersHere,
// other link options...
});
// Create Second Link
const secondLink = new HttpLink({
uri: "https://www.secondsource.com/api",
headers: yourHeadersHere,
// other link options...
});
function create(initialState) {
// Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
return new ApolloClient({
connectToDevTools: process.browser,
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
link: ApolloLink.split(
(operation) => operation.getContext().clientName === "second", // Routes the query to the proper client
secondLink,
firstLink
),
cache: new InMemoryCache().restore(initialState || {}),
});
}
export default function initApollo(initialState) {
// 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 (!process.browser) {
return create(initialState);
}
// Reuse client on the client-side
if (!apolloClient) {
apolloClient = create(initialState);
}
return apolloClient;
}
link
Parameter des Apollo Client zur Rettung
Beachten Sie die vorgenommenen Änderungen:
- Wir haben unseren ersten Link aus Gründen der Lesbarkeit ausgeklammert, da wir einen zweiten erstellen werden (beachten Sie, dass Sie hier beliebige Optionen an Apollo link modules übergeben können)
- Wir haben ApolloLink importiert, das als verfügbare Methode
.split()
hat. Diese Methode benötigt mindestens zwei Parameter (obwohl es nicht wirklich sinnvoll ist, weniger als drei zu übergeben): - Der erste ist eine Funktion, die entscheidet, welcher Link verwendet werden soll
- Der zweite ist der zu verwendende Link, wenn diese Funktion
true
zurückgibt. - Der dritte ist der zu verwendende Link, wenn die Funktion
false
zurückgibt. - An dieser Stelle haben Sie wahrscheinlich das Wesentliche verstanden, was hier vor sich geht.
An dieser Stelle haben Sie wahrscheinlich das Wesentliche verstanden, was hier vor sich geht.
Es geht nichts über ein bisschen Kontext, um zu verdeutlichen, was hier passiert...
Es bleibt jedoch die Frage, wie wir einer Komponente mitteilen, welche GraphQL-API sie abfragen soll? Indem wir den Kontext der Abfrage verwenden. Hier ist ein Beispiel:
const { data, error, loading } = useQuery(GET_STUFF, {
context: { clientName: "second" },
});
Hinweis: Ich verwende react-apollo-hooks' useQuery, um die Abfrage auszuführen, aber funktioniert genauso mit der Komponente Query, wenn Sie keine React-Hooks verwenden.
Der Sinn dieses Beispiels ist also, dass wir den Wert von context in unserer Abfrage und dann auf der Netzwerkseite (in unserer ApolloLink-Methode in init-apollo.js
) auf jeden beliebigen Wert setzen können. Achten Sie nur darauf, dass Sie an beiden Stellen die gleichen Werte verwenden.
Ich möchte den Link meines Apollo-Clients wie folgt einrichten:
// ...stuff
link: ApolloLink.split(
operation => operation.getContext().clientName === "second", // Routes the query to the proper client
secondLink,
firstLink
),
// ...stuff
Beachten Sie, dass der "firstLink" an zweiter Stelle angezeigt wird, was ihn zur Standarddatenquelle macht, die verwendet wird, wenn der in der Abfrage eingestellte Kontextwert nicht mit dem Wert übereinstimmt, nach dem die Funktion split
sucht. Das bedeutet, dass wir den Kontext nur dann setzen müssen, wenn wir unseren "Nicht-Standard"-Mandanten verwenden wollen.
Aktualisierung von with-apollo-client, damit wir vollständig gerenderte HTML-Exporte erhalten
Ok, wir kommen jetzt auf die Zielgerade, alles was wir tun müssen, ist unsere with-apollo-client.js
so zu bearbeiten, dass sie dem Folgenden entspricht:
import React from "react";
import initApollo from "./init-apollo";
import Head from "next/head";
import { getDataFromTree } from "react-apollo";
import { getMarkupFromTree } from "react-apollo-hooks";
import { renderToString } from "react-dom/server";
export default (App) => {
return class Apollo extends React.Component {
static displayName = "withApollo(App)";
static async getInitialProps(ctx) {
const { Component, router } = ctx;
let appProps = {};
if (App.getInitialProps) {
appProps = await App.getInitialProps(ctx);
}
// Run all GraphQL queries in the component tree
// and extract the resulting data
const apollo = initApollo();
if (!process.browser) {
try {
// Run all GraphQL queries
await getDataFromTree(
<App
{...appProps}
Component={Component}
router={router}
apolloClient={apollo}
/>
);
// Create static markup for SSR HTML export
await getMarkupFromTree({
renderFunction: renderToString,
tree: (
<App
{...appProps}
Component={Component}
router={router}
apolloClient={apollo}
/>
),
});
} 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();
}
// Extract query data from the Apollo store
const apolloState = apollo.cache.extract();
return {
...appProps,
apolloState,
};
}
constructor(props) {
super(props);
this.apolloClient = initApollo(props.apolloState);
}
render() {
return <App {...this.props} apolloClient={this.apolloClient} />;
}
};
};
Die einzigen Änderungen/Ergänzungen sind die beiden endgültigen Importe (getMarkupFromTree
& renderToString
) und die Hinzufügung des Abschnitts getMarkupFromTree, der es uns ermöglicht, vollständig gerenderte statische HTML-Exporte von Next.js zu haben.
Was aber, wenn ich mehr als zwei Datenquellen in meiner Anwendung benötige?
Witzig, dass Sie das fragen, denn der obige Trick (die .split()
operation on ApolloLink
) only works with up to two links. Bit of a chin scratcher at first, until you realize that the second link passed to the .split
method does not have to be an HttpLink
.
Dieser zweite Link kann ein weiterer ApolloLink
sein! Wie Sie unten sehen können, können wir einfach Links aneinanderreihen, bis wir den Kontext für alle möglichen Datenquellen überprüft haben:
// Create First Link
const firstLink = new HttpLink({
uri: "https://www.firstsource.com/api",
headers: yourHeadersHere,
// other link options...
});
// Create Second Link
const secondLink = new HttpLink({
uri: "https://www.secondsource.com/api",
headers: yourHeadersHere,
// other link options...
});
// Create Third Link
const thirdLink = new HttpLink({
uri: "https://www.thirdsource.com/api",
headers: yourHeadersHere,
// other link options...
});
const otherLinks = ApolloLink.split(
(operation) => operation.getContext().clientName === "third", // Routes the query to the proper client
thirdLink,
firstLink
);
function create(initialState) {
const client = new ApolloClient({
connectToDevTools: process.browser,
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
link: ApolloLink.split(
(operation) => operation.getContext().clientName === "second", // Routes the query to the proper client
secondLink,
otherLinks
),
cache: new InMemoryCache({ fragmentMatcher }).restore(initialState || {}),
});
return client;
}
Bonus: aber was ist mit mehreren Abfragen?
Wenn Sie sich mit mehreren Clients verbinden, ist es wahrscheinlich, dass Sie irgendwo in Ihrem Projekt zwei Abfragen in derselben Komponente ausführen müssen. Sicher, wir könnten compose von react-apollo
in unser Projekt importieren, aber das ist in diesem Fall wirklich nicht nötig (source). Wir können einfach unseren eigenen Hook erstellen (wenn Sie React Hooks verwenden). Das sieht dann in etwa so aus:
const queryMultiple = () => {
const res = useQuery(GET_POST, {
variables: {
uri: props.uri,
},
});
const res2 = useQuery(GET_SHOP, {
context: { clientName: "second" },
});
return [res, res2];
};
const [res, res2] = queryMultiple();
const { data, error, loading } = res;
const { data: secondData, error: secondError, loading: secondLoading } = res2;
Hut ab vor @ThomasK33 für seine Hilfe, dies herauszufinden.
Bonus #2: Was ist mit der Mutation bei mehreren Quellen?
Nach dem letzten Update hier (und ich werde diesen Beitrag aktualisieren, wenn sich die Situation ändert), scheint die Antwort zu sein: "Es kommt darauf an".
Wenn Sie ein <Mutation>
HOC verwenden, können Sie einfach Kontext an die Operation übergeben (docs) und Ihre Datenquelle auf ähnliche Weise auswählen, wie wir es oben getan haben.
Wenn Sie jedoch Kontext an useMutation aus der inoffiziellen react-apollo-hooks Bibliothek übergeben wollen, scheinen Sie derzeit kein Glück zu haben. Die gute Nachricht ist, dass die offizielle Apollo React Hooks Bibliothek derzeit in beta ist, und es scheint eine Gewissheit zu sein, dass sie Kontext als eine Eigenschaft in ihrer Version von useMutation einschließen würde, so dass eine Lösung hier wahrscheinlich um die Ecke ist.