Day10 在 Next.js 安装 apollo-graphql,串接 WordPress GraphQL API(下)

上一篇我们成功在 Next.js 安装 ApolloClient 了,今天我们继续使用刚安装的 ApolloClient 从 WordPress 抓取文章资料,在首页显示文章列表!

在 GraphiQL 决定需要的栏位,生成 query

首先打开 WordPress 後台的 GraphiQL IDE,在左边 Explorer 里面有各种可 query 的栏位,其中 posts 就是文章列表。展开它并勾选你想使用的栏位,如果不确定栏位意义的话,可以展开右边 Docs,里面可以找到每个栏位的简短说明文字,或是可以将所有想了解的栏位都打勾,直接按执行按钮,看回传值是什麽,这是比较快速的方式。

Imgur

我决定在我首页文章列表,使用图中所示的栏位,并且要依照日期由新到旧降序排序,以及只抓取前十篇,於是 GraphiQL 帮我生成的 query string 如下:

query MyQuery {
  posts(where: { orderby: { field: DATE, order: DESC } }, first: 10) {
    edges {
      node {
        databaseId
        title
        uri
        date
        excerpt
        featuredImage {
          node {
            sourceUrl
            altText
          }
        }
      }
    }
  }
}

Next.js 端实作

让我们把这段 query 复制进 Next.js 程序码里,我建立了一个 graphql 资料夹,用来存放所有 GraphQL query string 和资料型态转换的逻辑,并在里面建立了 allPostsQuery.js(完整路径是 /graphql/allPostsQuery.js),内容如下:

import { gql } from '@apollo/client'

import discardPTag from '../utils/discardPTag'

export const ALL_POSTS_QUERY = gql`
  query allPosts($first: Int!) {
    posts(where: { orderby: { field: DATE, order: DESC } }, first: $first) {
      edges {
        node {
          databaseId
          title
          uri
          date
          excerpt
          featuredImage {
            node {
              sourceUrl
              altText
            }
          }
        }
      }
    }
  }
`

export const allPostsQueryVars = {
  first: 10,
}

export const transformAllPostsData = (data) => {
  return (
    data?.posts?.edges
      ?.map((edge) => edge?.node)
      ?.map((post) => ({
        id: post?.databaseId || '',
        title: post?.title || '',
        uri: post?.uri || '',
        date: post?.date || '',
        excerpt: discardPTag(post?.excerpt) || '',
        featuredImage: {
          sourceUrl: post?.featuredImage?.node?.sourceUrl || '',
          altText: post?.featuredImage?.node?.altText || '',
        },
      })) || []
  )
}

里面放了从 GraphiQL 复制过来的 query string,并且我额外写了 transformAllPostsData function,用来在等等将 query 回来的资料转成用起来舒服的资料格式,把串接介面切乾净。

其中值得注意的是,excerpt 栏位我用来当作文章简介,这在编辑文章时有栏位可以输入,若没有输入的话,WordPress 也会自动生成,给文章一个摘要,但是这个栏位在 WPGraphQL query 回来时,是会多出前缀後缀 p tag 的,像是 <p>Real Content</p>,於是我在 transformAllPostsData 里面多用了另一个 discardPTag function 来移除多余 p tag,实作写在 /utils/discardPTag.js 里头:

/**
 * Discard starting and trailing <p> from a string
 * "<p>slice</p>" -> "slice"
 * @param {string} source
 * @returns string
 */
export default function discardPTag(source) {
  return source?.slice(3, -4)
}

接着在首页的 /pages/index.js 里,参照 Next.js 官方 with-apollo 范例写法,在最下面新增 getStaticProps function 来在 server side 产生 ApolloClient 并 query GraphQL API,并在 component 内部使用 useQuery 来使用 query 回来的资料,并用刚刚的 transformAllPostsData 转换资料,存成 allPosts 阵列,并在 render 时用 array.map 产生文章列表,修改完的完整 index.js 长这样:

import { useMemo } from 'react'
import Head from 'next/head'
import Image from 'next/image'
import Link from 'next/link'
import { useQuery } from '@apollo/client'

import styles from '../styles/Home.module.css'

import { initializeApollo, addApolloState } from '../lib/apolloClient'
import { allPostsQueryVars, ALL_POSTS_QUERY, transformAllPostsData } from '../graphql/allPostsQuery'

export default function Home() {
  const { data } = useQuery(ALL_POSTS_QUERY, {
    variables: allPostsQueryVars,
  })
  const allPosts = useMemo(() => transformAllPostsData(data), [data]) || []

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>就。很。Pro。blog</h1>

        <div className={styles.grid}>
          {allPosts?.map((post) => (
            <Link key={post.id} href={post.uri} passHref>
              <a className={styles.card}>
                <h2>{post.title}</h2>
                <p>{post.excerpt}</p>
              </a>
            </Link>
          ))}
        </div>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <span className={styles.logo}>
            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
          </span>
        </a>
      </footer>
    </div>
  )
}

export async function getStaticProps() {
  const apolloClient = initializeApollo()

  await apolloClient.query({
    query: ALL_POSTS_QUERY,
    variables: allPostsQueryVars,
  })

  return addApolloState(apolloClient, {
    props: {},
    revalidate: 1,
  })
}

最後为了让样式稍微能看,限制文章 title 和简介的行数,我修改了 /styles/Home.module.css,在大约 100 行处的 .card h2 和 .card p 都加了 line-clamp 的样式,修改完长这样:

/* ... */
/* At about line 100 */
.card h2 {
  margin: 0 0 1rem 0;
  font-size: 1.5rem;

  display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
	text-overflow: ellipsis;
  overflow: hidden;
}

.card p {
  margin: 0;
  font-size: 1.25rem;
  line-height: 1.5;

  display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
	text-overflow: ellipsis;
  overflow: hidden;
}
/* ... */

最後执行 yarn dev,打开浏览器进到 http://localhost:3000/ ,你应该就会看到如下画面,首页成功显示了基本文章列表!恭喜你!

Imgur

这篇文章的相关改动,可以参照这支 commit

下一篇

下一篇我们会继续来优化首页的样式,我们会安装 Tailwindcss 这套最近非常热门的 CSS 框架,来简单切版!


<<:  D9 - 彭彭的课程#Python 流程控制:if 判断式

>>:  Alpine Linux Porting (一点三?)

## [Day27] Video Speed Controller UI

[Day27] Video Speed Controller UI 需要用到的技巧与练习目标 mou...

[DAY 02]环境建置

先把环境建置好 环境建置 1.安装Anaconda 官方载点 步骤 安装注意事项 第三步选justm...

LineBot - 图文选单

昨天已经把 LineBot 设定好了,今天要做一些简单的指令,包含一图文选单,做完之後大概如下图: ...

Hello WebGL

大家好,大家都叫我西瓜。因为想转职写游戏,而游戏中会让人第一个想到、也是能在第一瞬间吸引人的就是画面...

Day12 Let's ODOO: Security(1) Access right

藉由ODOO的security,进行对model的权限设定,我们今天来写一个student 权限的范...