這系列的程式碼在 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 來,這邊用的是 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 都抓出來
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
}
接著就是把蒐集來的 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)
做好之後當然是要來使用了
// 省略部份 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 相關的功能都完成了,下一篇預定要來處理圖片檔