kevin wang's blog

透過 GraphQL 在 Gatsby 中做資料撈取

October 04, 2020

本文會提到

  1. Gatsby 的資料撈取機制
  2. 舉例說明
  3. 一個實際例子
  4. 從 第三方 API 作為資料來源產生畫面

在之前介紹 Gatsby 的文章中
我跳過了在 Gatsby 中使用 GraphQL,
在近期修改了 Blog 的樣式,
也補齊了一些之前被我跳過的知識,
這篇文章會記錄我學習 Gatsby 中使用 GraphQL 的一些筆記。

GraphQL 查詢互動介面

GraphQL 被整合在 Gatsby 之中作為 Data layer,
在 Gatsby Project 資料夾下執行 gatsby developgatsby build 後可以在

http://localhost:8000/___graphql

以互動網頁的方式查詢 GraphQL。

從 Gatsby Hello-World Project 起手

在 terminal 下以下指令新增專案

gatsby new hello-world https://github.com/gatsbyjs/gatsby-starter-hello-world

從左側點選樹狀或是直接輸入底下查詢

query MyQuery {
  site {
    siteMetadata {
      title
      description
    }
  }
}

會拿到以下結果

{
  "data": {
    "site": {
      "siteMetadata": {
        "title": null,
        "description": null
      }
    }
  },
  "extensions": {}
}

這個 Query 是站台的 metadata,
要更改 gatsby-config.js 增加設定,
參考以下程式碼

module.exports = {
  siteMetadata: {
    title: `some tile`,    
    description: `some description.`,
  },
  /* Your site config here */
  plugins: [],
}

重新 Query 就可以拿到以下結果

{
  "data": {
    "site": {
      "siteMetadata": {
        "title": "some tile",
        "description": "some description."
      }
    }
  },
  "extensions": {}
}

如果想要在 Gatsby 中使用這組資料,
則必須使用 graphql() 取得。
參考以下程式碼

import React from "react"
import { graphql }  from "gatsby"

export default function Home(data) {
  return <div>{data.data.site.siteMetadata.title}</div>
}

export const pageQuery = graphql`
  query {
    site {
      siteMetadata {
        title
        description
      }
    }
  }
`

就可以在畫面上看到剛剛設定的 some title。

其實傳進來的參數是一個物件,
所以我們可以改寫成

export default function Home({data}) {
  return <div>{data.site.siteMetadata.title}</div>
}

上面這種 Query 在 Gatsby 被稱為 Page Query。
其中 graphql 是一種 tag function
這是一種 JavaScript 的函數宣告,
細節原理可以參考 Gatsby 官方網站

還有一種查詢方式稱為 StaticQuery
以上面的範例來修改的話會變成

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

export default function Home() {
  return (
    <StaticQuery
      query={graphql`
        query {
          site {
            siteMetadata {
              title
              description
            }
          }
        }
      `}
      render={data => (
        <div>{data.site.siteMetadata.title}</div>
      )}
    />
  )
}

這部分的文件可以參考 Gatsby 官方網站


實際應用的例子

如果有使用 gatsby blog starter 的話,
就會知道頁面是放在 content/blog/ 底下,
實際上這是透過 gatsby-source-filesystem 這個模組定義了檔案的位置,
再透過 gatsby-transformer-remark 這個模組將 MD 檔案編譯成 html。
但是在 starter 的設計中,
會無法增加文章以外的頁面。
這部分要改動的話,
就要先了解 Gatsby 新增頁面的原理。

Gatsby 是怎麼增加頁面的

gatsby-node.js 中有提供許多建立頁面的 API 讓開發者可以產生出頁面。

createPages 這個 API 會負責建立頁面。
這個 API 會在資料都初始化完成後被呼叫,

而在 Gatsby Blog Starter Project 中,
createPages API 會與 GraphQL data layer 交互產生出頁面。

在 Gatsby Blog Starter Project 中要新增一篇文章,
必須要在 content/blog 資料夾內產生文章的資料夾。
如果想要增加一頁 about 頁的時候,
我們可以新增一個資料夾來放頁面,
比方像是 content/page
並在 gatsby-config.js 內使用 gatsby-source-filesystem 模組讓 Gatsby 認識 page 資料夾,
才能在編譯的時候讓 gatsby-transformer-remark 編譯到資料夾內的 md 檔。

參考以下設定

    {
      resolve: `gatsby-source-filesystem`,
      options: {
        path: `${__dirname}/content/page`,
        name: `assets`,
      },
    }

設定完且新增完 about 後,
就可以在新產生的靜態網頁中找到 /about/ 頁,
但這時候 about 頁也會在部落格文章列表中。

調整 新增頁面 GraphQL 以及查詢文章列表 GraphQL

在 Gatsby Blog Starter 的 gatsby-node.js 中,
可以看到 createPage 透過 query allMarkdownRemark 產生出文章列表的資料來產生文章相關檔案,
並在 index.js 內產生文章列表時透過 query allMarkdownRemark 產生文章列表畫面,
所以要做的調整有

  1. index.js 內的 query 增加 filter,
  2. 新增 about 頁要用的 templete
  3. gatsby-node.js 中的 createPage 增加新增 about 頁的相關邏輯
  4. 預設的 Gatsby Blog Starter 有使用 gatsby-plugin-feed 模組,這是一個產生 RSS 的模組,要調整成指定文章的內容才產生 RSS Feed。

參考以下程式碼

query 增加 filter

 `
  {
    allMarkdownRemark(
      sort: {fields: [frontmatter___date], order: DESC},
      filter: {fileAbsolutePath: {regex: "/content/blog/"}}
    ) {
      edges {
        node {
          fields {
            slug
          }
          frontmatter {
            title
          }
        }
      }
    }
  }
`

新增 templete 以及 CreatePage 可以參考 Starter 內現有 blog-post.js 產生方式調整。

參考以下程式碼

exports.createPages = async ({ graphql, actions }) => {
  await Promise.all([
    onCreateBlogPostPage(graphql,actions), 
    onCreatePagePostPage(graphql,actions)])
}

function onCreatePagePostPage(graphql, actions)
{
  const pagePostResultTask = graphql(
    `
      {
        allMarkdownRemark(
          sort: {fields: [frontmatter___date], order: DESC},
          filter: {fileAbsolutePath: {regex: "/content/page/"}}
        ) {
          edges {
            node {
              fields {
                slug
              }
              frontmatter {
                title
              }
            }
          }
        }
      }
    `
  ).then(function (pagePostResult){
    const { createPage } = actions

    const pagePost = path.resolve(`./src/templates/page-post.js`)
      
    if (pagePostResult.errors) {
      throw pagePostResult.errors
    }
  
    // Create page posts pages.
    const posts = pagePostResult.data.allMarkdownRemark.edges
  
    posts.forEach((post) => {      
  
      createPage({
        path: post.node.fields.slug,
        component: pagePost,
        context: {
          slug: post.node.fields.slug,
        },
      })
    })
  })
  return pagePostResultTask;
}

同場加映-在 Gatsby 中使用 第三方 API 當作來源

從上面的範例,
我們實作了 Gatsby 在 gatsby-node.js 透過 GraphQL 語法取得資料在編譯階段產生畫面,
除了從 Gatsby 專案內取得資料,
我們也可以從外部 API 取得資料,
官方做了一個範例舉例如何在編譯階段使用外部 API 產生畫面。
大致原理是在 createPage 時呼叫 Web API,
如果要使用 GraphQL Server 的資料當成產生畫面來源可以使用 gatsby-source-graphql 這個官方模組。
透過這些設計就可以使用 WordPress 等 CMS 的資料當成資料來源在編譯階段產生畫面。

在執行階段使用 第三方 API 當作來源

從上面資料可以知道 Gatsby 提供以第三方 API 當作來源在編譯階段取得資料後產生畫面。
如果是要一般使用者看到網頁後(執行階段)才呼叫 API 渲染畫面,
可以考慮用 React Component 內建的 componentDidMount() 搭配 setState 來實作。

首先新增一個 time-server.js 並執行

node time-server.js  

參考以下程式碼

var http = require('http'); 
var server = http.createServer(function (req, res) { 
     res.writeHead(200,{
          'Content-Type':'application/json',
          "Access-Control-Allow-Origin": "*"
     });
     res.write('{"time":"'+ new Date() +'"}');
     res.end(); 
}); 
server.listen(5000);
console.log('Node.js web server at port 5000 is running..')

增加 src/components/timepage.js

import React, { Component }  from 'react';

class TimePage extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        error: null,
        loading: true,
        items: []
      };
    }
  
    componentDidMount() {
      fetch("http://localhost:5000/")
        .then(res => res.json())
        .then(
          (result) => {
            this.setState({
                loading: false,
                time: result.time
            });
          },          
          (error) => {
            this.setState({
                loading: false,
              error
            });
          }
        )
    }
  
    render() {
      const { error, loading, time } = this.state;
      console.log(time,this.state);
      if (error) {
        return <div>Error: {error.message}</div>;
      } else if (loading) {
        return <div>Loading...</div>;
      } else {
        return (
        <div>{time}</div>
        );
      }
    }
}

export default TimePage

componentdidmount() 這個方法會在 DOM 被插到 DOM Tree 後被呼叫,
文件可以參考 React 官方網站

修改 index.js

import React from "react"
import TimePage from "../components/timepage"

export default function Home() {
  return <TimePage></TimePage>
}

將 Gatsby 跑起來以後畫面上就能出現從 API 取回的最新時間了。

ref:

  1. https://www.gatsbyjs.com/docs/graphql-concepts/
  2. https://www.gatsbyjs.com/docs/page-query/
  3. https://www.gatsbyjs.com/docs/static-query/
  4. https://www.gatsbyjs.com/docs/graphql/
  5. https://www.gatsbyjs.com/docs/node-apis/
  6. https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-transformer-remark
  7. https://www.gatsbyjs.com/docs/graphql-reference/
  8. https://reactjs.org/docs/react-component.html#componentdidmount