在Gatsby GraphQL中組合出完美資料


在Gatsby GraphQL中組合出完美資料

文章目錄

  1. Gatsby初試啼聲
  2. 利用react-bootstrap打造React風格頁面
  3. 了解Gatsby中GraphQL如何運作
  4. Gatsby GraphQL讀取JSON
  5. 在Gatsby GraphQL中組合出完美資料
  6. Gatsby程序化產生頁面
  7. 上線個人頁面到Netlify+啟動Netlify CMS

在上一篇文章中我們安裝了外掛,並且成功的讀取了我們存放在JSON中的資料。雖然GraphQL好用,可以用很好理解的query指定我們要讀取的資料格式,但是有時候回傳的資料還不是我們想要最完美的格式。到底是怎麼一回事呢?

製作成果展示頁面

我們還有一個成果展示頁面還沒有製作出來。如同上一篇文章,我們先新增一個新頁面work.js
src/pages/work.js

import React from "react";
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';

import Layout from "../components/layout";
import SEO from "../components/seo";
import Portfolio from "../components/portfolio";

class WorkPage extends React.Component {
  render() {
    return (
      <Layout>
        <SEO 
          title="Work"
          keywords={[`blog`, `Herbert Lin`, `javascript`, `react`, `gatsby`]}
        />
        <Row>
          <Col>
            <h2 className="mb-4">Work</h2>
          </Col>
        </Row>
        <Portfolio />
      </Layout>
    );
  }
}

export default WorkPage;

這裡因為預計要跟首頁共用Component,所以我就直接寫上匯入一個叫做PortfolioComponent。在製作Portfolio的時候會加上顯示多少案例的限制,這樣在首頁就可以只顯示部份案例,不至於讓首頁拉太長。
src/components裡面新增portfolio.js
src/components/portfolio.js

import React from "react";

class Portfolio extends React.Component {
  render() {
    return (
      <div></div>
    );
  }
}

export default Portfolio;

先空著,搞定資料格式後等一下再回來補上剩下的內容。

從GraphQL中調取圖片

關於成果展示的部份,我們也用讀取的方式來得到成果資料,這樣以後改JSON就可以更新資料了。先根據在上一篇文章Gatsby GraphQL讀取JSON學到的知識,我們可以新建另一個JSON檔存放關於成果的資料。
src/content/data/work.json

[
  {
    "title": "XChange",
    "description": "The redesigned website for XChange which also serves the purpose of being a media package.",
    "image": "xchange.png"
  },
  {
    "title": "Stihl Taiwan",
    "description": "The revamped brand website for Stihl Taiwan.",
    "image": "stihl.png"
  },
  {
    "title": "LinkMyGoods",
    "description": "LinkMyGoods is a CRM system designed to smoothen out the trading process from quotation to shipping.",
    "image": "linkmygoods.png"
  }
]

以上的資料可以透過下面的query讀取:

query WorkQuery {
  allWorkJson {
    nodes {
      description
      image
      title
    }
  }
}

加入了work.json之後,我們可以發現在互動工具中又新增了兩個項目allWorkJsonworkJson。這兩者的區別是allWorkJson會回傳所有的node,而workJson只會回傳一個,所以如果我們只想要某個特定的案子,我們可以向workJson傳入比較參數。

至於圖片,在Gatsby中有提供處理影像的外掛gatsby-plugin-sharp以及gatsby-transformer-sharp。配合讀取檔案的外掛gatsby-source-filesystem我們就可以輕鬆的從GraphQL讀取圖片。
一個從GraphQL要圖片的query範例是:

query {
  file(relativePath: { eq: "images/default.jpg" }) {
    childImageSharp {
      # Specify a fixed image and fragment.
      # The default width is 400 pixels
      fixed {
        ...GatsbyImageSharpFixed
      }
    }
  }
}

不過在這裡我想要讀取所有的圖片,因為我的圖片名稱存在JSON裡面我不想要指定,但是相對的我得必須知道圖片名稱是什麼,這樣之後才能夠找到。還有圖片我也不想要固定大小的,我想讓他可以隨著元素大小調整,所以我把query改成下面的形式:

query ImageQuery {
  allFile {
    nodes {
      name
      childImageSharp {
        fluid {
          ...GatsbyImageSharpFluid
        }
      }
    }
  }
}

再把GraphQL串接到Portfolio中:
src/components/portfolio.js

import React from "react";
import { StaticQuery, graphql } from "gatsby";

class Portfolio extends React.Component {
  render() {
    return (
      <StaticQuery
        query={portfolioQuery}
        render={data => {
          const { portfolioData, images } = data;
          return (

          );
        }}
      />
    );
  }
}

const portfolioQuery = graphql`
  query WorkQuery {
    portfolioData: allWorkJson {
      nodes {
        description
        image
        title
      }
    }
    images: allFile {
      nodes {
        childImageSharp {
          fluid {
            ...GatsbyImageSharpFluid
          }
        }
      }
    }
  }
`;

export default Portfolio;

簡單加上bootstrap風格卡片:

import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Card from 'react-bootstrap/Card';
import Img from "gatsby-image";

class Portfolio extends React.Component {
  render() {
    const limit = this.props.limit | null;

    return (
      <StaticQuery
        query={portfolioQuery}
        render={data => {
          const { images } = data;
          const portfolioData = limit ? data.portfolioData.nodes.slice(0, limit) : data.portfolioData.nodes;
          console.log('data', data);
          return (
            <Row>
              {portfolioData.map(work => {
                const imageIndex = images.nodes.findIndex(x => x.name === work.image.split('.')[0]);
                return (
                  <Col md={4} className="work" key={work.title}>
                    <Card>
                      <Card.Img 
                        variant="top"
                        as={Img}
                        fluid={images.nodes[imageIndex].childImageSharp.fluid}
                      />
                      <Card.Body>
                        <Card.Title>{work.title}</Card.Title>
                        <Card.Text>{work.description}</Card.Text>
                      </Card.Body>
                    </Card>
                  </Col>
                );
              })}
            </Row>
          );
        }}
      />
    );
  }
}
  1. 這裡使用gatsby-image所提供的元件,是對外掛gatsby-plugin-sharp和響應式固定寬高圖片與全版圖片進行優化過的元件,僅供Gatsby內部圖片使用。
  2. 因為從GraphQL得到是所有圖片的資料,所以我們要先比對圖片名字,找到我們要的圖片的索引。
  3. 為了在首頁也可以使用這個元件,加上limit屬性。

最後不要忘記在src/content/assets裡面加入相對應的圖片。

work.js實際成果如下:

首頁中也加入Portfolio,並傳入limit屬性。在首頁我設的數量限制是6個,這個可以自行調整。
src/pages/index.js

import Portfolio from "../components/portfolio";
...
        <Hero className="px-0">
          <h1>Hi! I'm a person.</h1>
          <p>
            I am super duper.
            <br />
            I am looking at a personal page & blog example on github.
          </p>
          <Link to="/about/">
            <Button variant="primary">Learn more about me</Button>
          </Link>
        </Hero>
        <Row>
          <Col>
            <h2 className="mb-4">Recent work</h2>
          </Col>
        </Row>
        <Portfolio limit={6} />
        <Row className="mt-4">
          <Col>
            <Link to="/work/">
              <Button variant="primary">View more</Button>
            </Link>
          </Col>
        </Row>
...

首頁實際成果如下:

GraphQL更上一層樓

既然我們有辦法讀取JSON,也可以讀取圖片,都是從GraphQL要資料,那我們有沒有辦法把這兩個資料組合在一起丟出來,不要再找對應的圖片了?完全可以做到,但是我們要自己資料組合起來,打造出客製化的資料格式。

這個步驟如果沒有興趣的朋友,可以直接跳過。後面的程式碼還是會沿用這個步驟的結果。這裡只是更深入Gatsby GraphQL系統,做資料讀取的優化。

我們可以用Gatsby的sourceNodesAPI製作客製化的資料。以下內容轉自這篇教學,裡面有更詳細的說明。
gatsby-node.js

const path = require('path');
// 匯入work.json
const work = require('./content/data/work.json');

// 存放圖片的相對路徑
const IMAGE_PATH = './content/assets/';
...

exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => {
  work.forEach((project, index) => {
    // 1. 讀取JSON資料
    const {
      title,
      description,
      image,
    } = project;

    const idTitle = title.split(' ')
      .map(elem => elem.toLowerCase())
      .join('-');

    // 2. 客製化節點的架構
    const node = {
      title,
      description,
      image, // 這裡有問題
      id: createNodeId(`work-${idTitle}`),
      internal: {
        type: 'WorkProject',
        contentDigest: createContentDigest(project),
      },
    };

    // 3. 建立節點
    actions.createNode(node);
  });
};

因為id必須是獨特的,所以我在處理過標題後,把idTitle插入id中。到目前為止我們已經製作出客製化的節點了,但是裡面圖片的資料卻還是我們之前舊的格式。那應該如何製作圖片的節點呢?

exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => {
  work.forEach((project, index) => {
    const {
      title,
      description,
      image,
    } = project;

    // 1. 製作圖片的節點必須有名稱、副檔名、以絕對路徑
    const { name, ext } = path.parse(image);
    const absolutePath = path.resolve(__dirname, IMAGE_PATH, image);

    // 2. 製作影像處理外掛`sharp`可以認得的架構
    const imageData = {
      name,
      ext,
      absolutePath,
      extension: ext.substring(1)
    };

    // 3. 包括上面的架構在內,製作節點的整體架構
    const imageNode = {
      ...imageData,
      id: createNodeId(`work-image-${name}-${index}`),
      internal: {
        type: 'WorkProjectImage',
        contentDigest: createContentDigest(imageData),
      },
    };

    // 4. 建立影像節點。在建立的過程`sharp`會把childImageSharp放進去
    actions.createNode(imageNode);

    const idTitle = title.split(' ')
      .map(elem => elem.toLowerCase())
      .join('-');

    const node = {
      title,
      description,
      image: imageNode, // 5. 把新建立的圖片節點加到原本的節點中
      id: createNodeId(`work-${idTitle}`),
      internal: {
        type: 'WorkProject',
        contentDigest: createContentDigest(project),
      },
    };

    actions.createNode(node);
  });
};

之後我們把portfolio.js改成新query並修改一下相對應欄位就大公告成了。
src/components/portfolio.js

class Portfolio extends React.Component {
  render() {
    const limit = this.props.limit | null;

    return (
      <StaticQuery
        query={portfolioQuery}
        render={data => {
          const portfolioData = limit ? data.portfolioData.nodes.slice(0, limit) : data.portfolioData.nodes;
          return (
            <Row>
              {portfolioData.map(work => {
                return (
                  <Col className="work" key={work.title}>
                    <Card>
                      <Card.Img 
                        variant="top"
                        as={Img}
                        fluid={work.image.childImageSharp.fluid}
                      />
                      <Card.Body>
                        <Card.Title>{work.title}</Card.Title>
                        <Card.Text>{work.description}</Card.Text>
                      </Card.Body>
                    </Card>
                  </Col>
                );
              })}
            </Row>
          );
        }}
      />
    );
  }
}

const portfolioQuery = graphql`
  query WorkQuery {
    portfolioData: allWorkProject {
      nodes {
        description
        title
        image {
          childImageSharp {
            fluid {
              ...GatsbyImageSharpFluid
            }
          }
        }
      }
    }
  }
`;

本篇文章的程式碼的實作可以在github repo中 5-creating-custom-data-with-image 找到

參考資源

#React #gatsby #graphql
現在很多網站都不需要複雜的結構,使用靜態網站呈現就可以了。可以產生靜態網站的框架有很多,這次介紹使用React系列的Gatsby來快速製作一個個人頁面。






Related Posts

初識 shell script

初識 shell script

JavaScript  與瀏覽器的溝通 : DOM

JavaScript 與瀏覽器的溝通 : DOM

簡明 Scratch 小遊戲開發入門教學

簡明 Scratch 小遊戲開發入門教學



Comments