问题
In the Contentful CMS, I have two different content-types: BigCaseStudy
and BigCaseStudySection
. To get this content to appear in my Gatsby 2.x site, my thinking was:
- Do query 1, which gets all the
BigCaseStudy
fields I want to display, and also contains the content'sID
field as metadata. - Take that
ID
from query 1, match to a Contentful reference field (which contains anID
) in query 2 - Do query 2, return all matching
BigCaseStudySection
fields
The end goal would be to display the original BigCaseStudy
with all of the BigCaseStudySection
(usually numbering between 3-5 of them). You can look at my queries to see the fields, there are bunch.
I think some combination of GraphQL variables and queries would get me there (maybe a mutation)? It's not clear and I haven't seen any complex examples of querying one set of stuff and then using the response to make another call, like chained promises or async/await in JS.
Any ideas on the right construction?
bigcasestudy.js
component with GraphQL queries:
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { graphql } from 'gatsby'
import Img from 'gatsby-image'
import Layout from '../Layout/layout'
/**
* Hero Section
*/
const HeroContainer = styled.header`
align-items: center;
background-image: url(${ props => props.bgImgSrc });
background-position: center center;
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
height: calc(100vh - 128px);
`
const HeroTitle = styled.h1`
color: #fff;
font-size: 70px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 15px;
`
const HeroSubtitle = styled.h2`
color: #fff;
font-size: 24px;
font-weight: 300;
letter-spacing: 5px;
text-transform: uppercase;
`
/**
* Intro Section
*/
const IntroBG = styled.section`
background-color: ${ props => props.theme.lightGray };
padding: 50px 0;
`
const IntroContainer = styled.div`
padding: 25px;
margin: 0 auto;
max-width: ${ props => props.theme.sm };
@media (min-width: ${ props => props.theme.sm }) {
padding: 50px 0;
}
`
const IntroTitle = styled.h2`
font-size: 50px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 45px;
text-align: center;
`
const IntroText = styled.p`
font-size: 22px;
line-spacing: 4;
text-align: center;
`
const IntroButton = styled.a`
background-color: #fff;
color: ${ props => props.theme.darkGray };
border: 1px solid ${ props => props.theme.mediumGray };
border-radius: 25px;
display: block;
font-size: 16px;
letter-spacing: 5px;
margin: 30px auto;
padding: 15px 45px;
text-align: center;
text-decoration: none;
text-transform: uppercase;
width: 300px;
`
// BigCaseStudy Component
class BigCaseStudy extends React.Component {
render() {
// Setup destructured references to each Contentful object passed in through the GraphQL call
const { caseStudyTitle } = this.props.data.contentfulBigCaseStudy
const { caseStudySubtitle } = this.props.data.contentfulBigCaseStudy
const { caseStudyIntroTitle } = this.props.data.contentfulBigCaseStudy
const { caseStudyIntro } = this.props.data.contentfulBigCaseStudy.caseStudyIntro
const { caseStudyLink } = this.props.data.contentfulBigCaseStudy
console.log(this)
return (
<Layout>
<HeroContainer
bgImgSrc={ this.props.data.contentfulBigCaseStudy.caseStudyHero.fixed.src }>
<HeroTitle>{ caseStudyTitle }</HeroTitle>
<HeroSubtitle>{ caseStudySubtitle }</HeroSubtitle>
</HeroContainer>
<IntroBG>
<IntroContainer>
<IntroTitle>{ caseStudyIntroTitle }</IntroTitle>
<IntroText>{ caseStudyIntro }</IntroText>
</IntroContainer>
<IntroButton href={ caseStudyLink } target="_blank" rel="noopener noreferrer">
Visit the site >
</IntroButton>
</IntroBG>
</Layout>
)
}
}
// Confirm data coming out of contentful call is an object
BigCaseStudy.propTypes = {
data: PropTypes.object.isRequired
}
// Export component
export default BigCaseStudy
// Do call for the page data
// This needs to mirror how you've set up the dynamic createPage function data in gatsby-node.js
export const BigCaseStudyQuery = graphql`
query BigCaseStudyQuery {
contentfulBigCaseStudy {
id
caseStudyTitle
caseStudySubtitle
caseStudyIntroTitle
caseStudyIntro {
caseStudyIntro
}
caseStudyLink
caseStudyHero {
fixed {
width
height
src
srcSet
}
}
},
contentfulBigCaseStudySection (id: $postId) {
title
order
images {
fixed {
width
height
src
srcSet
}
}
bigCaseStudyReference {
id
}
body {
body
}
stats {
stat1 {
word
number
}
stat2 {
word
number
}
stat3 {
word
number
}
stat4 {
word
number
}
}
id
}
}
`
gatsby-node.js
file:
/**
* Implement Gatsby's Node APIs in this file.
*
* ######################################################
* BIG CASE STUDY BACKEND CODE
* ######################################################
*
* We are using the .createPages part of the Gatsby Node API: https://next.gatsbyjs.org/docs/node-apis/#createPages
* What this does is dynamically create pages (surprise) based on the data you feed into it
*
* Feed the contentful API call into the promise
* Here I'm calling BigCaseStudy, which is a custom content type set up in contentful
* This is briefly explained over here: https://www.gatsbyjs.org/packages/gatsby-source-contentful/
*
* Also, note the caseStudyIntro field is long text `markdown`
* Gatsby returns the long text field as an object
* Calling it's name inside of the object returns the HTML
* Read more here: https://github.com/gatsbyjs/gatsby/issues/3205
*/
// Set Gatsby path up to be used by .createPages
const path = require('path')
// Using Node's module export, Gatsby adds in a createPages factory
exports.createPages = ({ graphql, actions }) => {
// We setup the createPage function takes the data from the actions object
const { createPage } = actions
// Setup a promise to build pages from contentful data model for bigCaseStudies
return new Promise((resolve, reject) => {
// Setup destination component for the data
const bigCaseStudyComponent = path.resolve('src/components/BigCaseStudy/bigcasestudy.js')
resolve(
graphql(`
{
allContentfulBigCaseStudy {
edges {
node {
id
caseStudySlug
caseStudyTitle
caseStudySubtitle
caseStudyIntroTitle
caseStudyIntro {
caseStudyIntro
}
caseStudyLink
caseStudyHero {
fixed {
width
height
src
srcSet
}
}
}
}
}
allContentfulBigCaseStudySection {
edges {
node {
title
order
images {
fixed {
width
height
src
srcSet
}
}
bigCaseStudyReference {
id
}
body {
body
}
stats {
stat1 {
word
number
}
stat2 {
word
number
}
stat3 {
word
number
}
stat4 {
word
number
}
}
id
}
}
}
}
`).then((result) => {
// Now we loop over however many caseStudies Contentful sent back
result.data.allContentfulBigCaseStudy.edges.forEach((caseStudy) => {
const caseStudySections = result.data.allContentfulBigCaseStudySection.edges
let caseStudySectionMatches = caseStudySections.filter(
caseStudySection => caseStudySection.bigCaseStudyReference.id === caseStudy.id
)
createPage ({
path: `/work/${caseStudy.node.caseStudySlug}`,
component: bigCaseStudyComponent,
context: {
id: caseStudy.node.id,
slug: caseStudy.node.caseStudySlug,
title: caseStudy.node.caseStudyTitle,
subtitle: caseStudy.node.caseStudySubtitle,
hero: caseStudy.node.caseStudyHero,
introTitle: caseStudy.node.caseStudyIntroTitle,
intro: caseStudy.node.caseStudyIntro.caseStudyIntro,
link: caseStudy.node.caseStudyLink,
caseStudySection: caseStudySectionMatches.node
}
})
})
})
// This is the error handling for the calls
.catch((errors) => {
console.log(errors)
reject(errors)
})
) // close resolve handler
}) // close promise
}
回答1:
I ran into this challenge too and couldn't find a good solution to accomplish that (although I wasn't using Contentful), but I did work past it and think I can help. You'll need to shift your thinking a bit.
Basically, GraphQL isn't really meant to query for the data you need to run another query. It's more of a 'ask for what you need' sort of tool. GraphQL wants to run a single query for exactly what you need.
The parameter you need for your query actually comes from your gatsby-node.js
file. Specifically, the context property of createPages()(a Node API that gatsby makes available).
Is that enough to get you pointed in the right direction? If you need a bit more of a hand, then there are two things I need to know:
1. A little more context around what you're trying to accomplish. What is the specific data you want available to the end user?
2. What your gatsby-node.js
file looks like.
回答2:
Short answer: you don't do callbacks with GraphQL. You do one query that gets everything you need all at once.
Longer answer: I had to reconstruct how the gatsby-node.js
file fetched Contentful content and then filtered through it. In Gatsby, you want to set up the queries in gatsby-node.js
to go fetch everything from your data source because it's a static site generator. It's architecture brings all that data in and then parses it out accordingly.
The GraphQL query from my original question was fine. I changed .then()
of the promise to use .filter()
on my results, comparing the relationship field of the child nodes to the id of the parent nodes.
gatsby-node.js
:
// Set Gatsby path up to be used by .createPages
const path = require('path')
// Using Node's module export, Gatsby adds in a createPages factory
exports.createPages = ({ graphql, actions }) => {
// We setup the createPage function takes the data from the actions object
const { createPage } = actions
// Setup a promise to build pages from contentful data model for bigCaseStudies
return new Promise((resolve, reject) => {
// Setup destination component for the data
const bigCaseStudyComponent = path.resolve('src/components/BigCaseStudy/bigcasestudy.js')
resolve(
graphql(`
{
allContentfulBigCaseStudy {
edges {
node {
id
caseStudySlug
caseStudyTitle
caseStudySubtitle
caseStudyIntroTitle
caseStudyIntro {
caseStudyIntro
}
caseStudyLink
caseStudyHero {
fixed {
width
height
src
srcSet
}
}
}
}
}
allContentfulBigCaseStudySection {
edges {
node {
title
order
images {
fixed {
width
height
src
srcSet
}
}
bigCaseStudyReference {
id
}
body {
body
}
stats {
stat1 {
word
number
}
stat2 {
word
number
}
stat3 {
word
number
}
stat4 {
word
number
}
}
id
}
}
}
}
`).then((result) => {
// Now we loop over however many caseStudies Contentful sent back
result.data.allContentfulBigCaseStudy.edges.forEach((caseStudy) => {
let matchedCaseStudySections = result.data.allContentfulBigCaseStudySection.edges.filter(
caseStudySection =>
caseStudySection.node.bigCaseStudyReference.id === caseStudy.node.id
)
createPage ({
path: `/work/${caseStudy.node.caseStudySlug}`,
component: bigCaseStudyComponent,
context: {
id: caseStudy.node.id,
slug: caseStudy.node.caseStudySlug,
title: caseStudy.node.caseStudyTitle,
subtitle: caseStudy.node.caseStudySubtitle,
hero: caseStudy.node.caseStudyHero,
introTitle: caseStudy.node.caseStudyIntroTitle,
intro: caseStudy.node.caseStudyIntro.caseStudyIntro,
link: caseStudy.node.caseStudyLink,
caseStudySection: matchedCaseStudySections.node
}
})
})
})
// This is the error handling for the calls
.catch((errors) => {
console.log(errors)
reject(errors)
})
) // close resolve handler
}) // close promise
}
Once you've set this up, the createPage
part of Gatsby Node API sends the parent and all of it's nodes over to the component
param you set.
Inside of my component, I can now make a GraphQL query for all children nodes. That now returns what I want and conforms to the idea that GraphQL makes one request instead of multiple like I was trying to do. The only tricky part is that you have to use .map()
in the render part of the component to loop over all the child nodes sent back from Contentful.
bigcasestudy.js
component:
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { graphql } from 'gatsby'
import Img from 'gatsby-image'
import Layout from '../Layout/layout'
/**
* Hero Section
*/
const HeroContainer = styled.header`
align-items: center;
background-image: url(${ props => props.bgImgSrc });
background-position: center center;
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
height: calc(100vh - 128px);
`
const HeroTitle = styled.h1`
color: #fff;
font-size: 70px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 15px;
`
const HeroSubtitle = styled.h2`
color: #fff;
font-size: 24px;
font-weight: 300;
letter-spacing: 5px;
text-transform: uppercase;
`
/**
* Intro Section
*/
const IntroBG = styled.section`
background-color: ${ props => props.theme.lightGray };
padding: 50px 0;
`
const IntroContainer = styled.div`
padding: 25px;
margin: 0 auto;
max-width: ${ props => props.theme.sm };
@media (min-width: ${ props => props.theme.sm }) {
padding: 50px 0;
}
`
const IntroTitle = styled.h2`
font-size: 50px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 45px;
text-align: center;
`
const IntroText = styled.p`
font-size: 22px;
line-spacing: 4;
text-align: center;
`
const IntroButton = styled.a`
background-color: #fff;
color: ${ props => props.theme.darkGray };
border: 1px solid ${ props => props.theme.mediumGray };
border-radius: 25px;
display: block;
font-size: 16px;
letter-spacing: 5px;
margin: 30px auto;
padding: 15px 45px;
text-align: center;
text-decoration: none;
text-transform: uppercase;
width: 300px;
`
// BigCaseStudy Component
class BigCaseStudy extends React.Component {
render() {
// Destructure Case Study Intro stuff
const {
caseStudyHero,
caseStudyIntro,
caseStudyIntroTitle,
caseStudyLink,
caseStudySubtitle,
caseStudyTitle
} = this.props.data.contentfulBigCaseStudy
// Setup references to Case Study Sections, destructure BigCaseStudySection object
const caseStudySections = this.props.data.allContentfulBigCaseStudySection.edges.map(
(currentSection) => {
return currentSection.node
}
)
// Case Study Section can be in any order, so we need to sort them out
const caseStudySectionsSorted = caseStudySections.sort( (firstItem, secondItem) => {
return firstItem.order > secondItem.order ? 1 : -1
})
console.log(caseStudySectionsSorted)
return (
<Layout>
<HeroContainer
bgImgSrc={ caseStudyHero.fixed.src }>
<HeroTitle>{ caseStudyTitle }</HeroTitle>
<HeroSubtitle>{ caseStudySubtitle }</HeroSubtitle>
</HeroContainer>
<IntroBG>
<IntroContainer>
<IntroTitle>{ caseStudyIntroTitle }</IntroTitle>
<IntroText>{ caseStudyIntro.caseStudyIntro }</IntroText>
</IntroContainer>
<IntroButton href={ caseStudyLink } target="_blank" rel="noopener noreferrer">
Visit the site >
</IntroButton>
</IntroBG>
{
caseStudySectionsSorted.map( (caseStudySection, index) => {
return <IntroTitle key={ index }>{ caseStudySection.title }</IntroTitle>
})
}
</Layout>
)
}
}
// Confirm data coming out of contentful call is an object
BigCaseStudy.propTypes = {
data: PropTypes.object.isRequired
}
// Export component
export default BigCaseStudy
// Do call for the page data
// This needs to mirror how you've set up the dynamic createPage function data in gatsby-node.js
export const BigCaseStudyQuery = graphql`
query BigCaseStudyQuery {
contentfulBigCaseStudy {
id
caseStudyTitle
caseStudySubtitle
caseStudyIntroTitle
caseStudyIntro {
caseStudyIntro
}
caseStudyLink
caseStudyHero {
fixed {
width
height
src
srcSet
}
}
}
allContentfulBigCaseStudySection {
edges {
node {
title
order
images {
fixed {
width
height
src
srcSet
}
}
bigCaseStudyReference {
id
}
body {
body
}
stats {
stat1 {
word
number
}
stat2 {
word
number
}
stat3 {
word
number
}
stat4 {
word
number
}
}
id
}
}
}
}
`
H/t: thanks to @taylor-krusen for rearranging how I was approaching this problem.
来源:https://stackoverflow.com/questions/52360940/graphql-query-callbacks-for-gatsby-js