iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
1

這系列的程式碼在 https://github.com/DanSnow/ironman-2020/tree/master/static-site-generator

Gatsby 有個功能是 static query ,它跟 page query 不同的地方在於它可以使用在任何的元件中,這讓使用者可以不用自己用 props 來傳資料,而且它所取得的資料會直接內嵌在產生的 js 中,雖然還是有些限制,比如不能有變數,不過整體而言還是讓網頁變的有彈性的多

而它的真面目實際上是 Gatsby 會去每個檔案中找到 StaticQuery 或是 useStaticQuery ,並把裡面的 query 用 babel 取出來,將查詢完的結果放回 Redux 的資料中,不過我這邊打算用比較簡單一點的實作方式,我直接把 query 的查詢結果存到一個 json 檔中,然後用 babel plugin 來轉換成 import 那個 json 檔

從程式碼中找出 query

首先是如何找出 query 來,這邊用的是 babel ,關於 babel 的用法可以去看我的另一個系列的這篇,最簡單的方法就是找到 useStaticQuery 的呼叫,然後把裡面的字串找出來

import * as t from '@babel/types'
import { extractQueryString, normalizeQuery } from './utils'

export const visitor = {
  CallExpression(path, state) {
    if (!t.isIdentifier(path.node.callee, { name: 'useStaticQuery' })) {
      return
    }
    const query = extractQueryString(path.node)
    // normalizeQuery 其實也就只是把換行什麼的去掉而已
    state.queries.push(normalizeQuery(query))
  },
}

function extractQueryString(node) {
  // 就單純的 AST 的路徑,對應的是 useStaticQuery(gql`query`) 中的 query
  return node.arguments[0].quasi.quasis[0].value.raw
}

從檔案中蒐集 query

再來就是從所有的程式碼中把 query 都抓出來

import { resolve } from 'path'
import { readFile } from 'fs/promises'
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import { visitor } from './visitor'
import globby from 'globby'
import pMap from 'p-map'

export async function collectQuery() {
  // 一樣的 globby
  const files = await globby(resolve(process.cwd(), 'src/**/*.js'))
  const collections = await pMap(files, extractQuery)
  const queries = new Set(collections.flat())
  return Array.from(queries)
}

async function extractQuery(file) {
  const code = await readFile(file, 'utf-8')
  const ast = parse(code, {
    sourceType: 'module',
    // 這邊把可能用到的語法都打開
    plugins: ['jsx', 'nullishCoalescingOperator', 'optionalChaining', 'objectRestSpread'],
  })
  const state = { queries: [] }
  // 傳入自己的 state 用來取得結果
  traverse(ast, visitor, null, state)
  return state.queries
}

執行 Static Query

接著就是把蒐集來的 query 全部執行過一次,然後寫入檔案中:

// 省略 import
import objectHash from 'object-hash'
import { collectQuery } from './collect-query'

export async function executeStaticQueries(schema) {
  const base = resolve(process.cwd(), '.cache/queries')
  await mkdir(base, { recursive: true })
  // 剛剛的蒐集 query 的函式
  const queries = await collectQuery()
  await pMap(queries, async (query) => {
    const { data } = await execute(schema, gql(query))
    // 這邊用 object-hash 來產生一個 hash
    const hash = objectHash(query)
    writeFile(resolve(base, `${hash}.json`), JSON.stringify(data))
  })
}

轉換程式碼

最後一步驟就是轉換使用者的程式碼,讓使用者去 import 執行好的結果,這邊用的是 babel macro ,一樣不知道怎麼用的可以參考這篇

// 省略部份 require
const { createMacro } = require('babel-plugin-macros')
const { addDefault } = require('@babel/helper-module-imports')

function extractQuery({ references, state }) {
  // 處理所有的 useStaticQuery
  for (const path of references.useStaticQuery) {
    const query = extractQueryString(path.parent)
    // 用同樣的方式產生 hash
    const hash = objectHash(normalizeQuery(query))
    const p = join(state.cwd, `.cache/queries/${hash}.json`)
    // 新增 import
    const id = addDefault(path, relative(dirname(state.filename), p))
    // 換成 import 進來的資料
    path.parentPath.replaceWith(id)
  }
}

module.exports = createMacro(extractQuery)

使用 Static Query

做好之後當然是要來使用了

// 省略部份 import
import { gql } from 'generator'
import { useStaticQuery } from 'generator/macro'

export function ArticleList() {
  // 這邊就能取得所有文章了
  const { allArticles: articles } = useStaticQuery(gql`
    query {
      allArticles {
        slug
        title
        content
      }
    }
  `)
  return (
    <article className="space-y-8">
      {articles.map(({ slug, title, content }) => (
        <Link key={slug} className="block" to={`/articles/${slug}`}>
          <ArticlePreview title={title} content={content} />
        </Link>
      ))}
    </article>
  )
}

這樣 GraphQL 相關的功能都完成了,下一篇預定要來處理圖片檔


上一篇
Day 24: 用 GraphQL 取得動態路由
下一篇
Day 26: 載入圖片
系列文
從 0 開始建一個 Static Site Generator30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言