趣味で Gatsby をいじり始めて知見が溜まってきたのでメモる。
この知識を使った実際のソースは
拙作の gatsby-theme(GitHub)
に転がってるので、参考まで。
配列操作
Gatsby は配列操作を非常によく使う。
例えば、markdown の全記事は GraphQL で ↓ のように query する。
{
allMarkdownRemark {
nodes {
id
frontmatter {
tags
title
}
}
}
}
結果が data.allMarkdownRemark.nodes
に配列として格納される。
で、記事タイトルを集めて element にしようと思ったら、↓ のように Array.prototype.map()
を使う。
const titles = nodes.map(({ title }) => <p key={title}>{title}</p>);
他には、特定の tag ごとに記事を集めようと思ったら、 Array.prototype.filter()
を使う。
const filtered = nodes.filter(({ frontmatter: { tags } }) =>
tags.includes(`hoge`)
);
Array.prototype.reduce()
だけはあまり使わないかな。
分割代入
配列の中身や、オブジェクトのプロパティを直接引っ張り出して変数に代入するやつ。
例えば、全記事を query した結果を ↓ のように処理してもよいが、
const nodes = data.allMarkdownRemark.nodes;
// 以下、 `nodes` を使ってあれこれ
分割代入を使って ↓ のように書くともっとスマート。
const {
allMarkdownRemark: { nodes },
} = data;
// 以下、 `nodes` を使ってあれこれ
実際には、 query の結果を分割代入することが多い。
↓ は useStaticQuery()
を使う場合の例。
const {
site: { siteMetadata },
} = useStaticQuery(
graphql`
query {
site {
siteMetadata {
author {
name
summary
}
}
}
}
`
);
// 以下、 `siteMetadata` を使ってあれこれ
後は、React のコンポーネントの関数定義でもよく使う。 children
とか ↓。
import * as React from "react";
export const ButtonBase = ({ children }) => (
<div className="flex min-w-[2rem] items-center justify-center whitespace-nowrap p-2">
{children}
</div>
);
注意としては、オブジェクトを掘り下げていく途中で null
になる場合はエラーになる。
なので、確実に null
じゃないプロパティまで分割代入する。
その続きは、↓ のようにオプショナルチェーンを使って null
でも大丈夫なようにする。
const name = siteMetadata?.author?.name || `NO NAME`;
const summary = siteMetadata?.author?.summary || ``;
分割代入時に名前を変えることもできる。
const obj = { a: 1, b: 2, c: { x: 4, y: 5, z: 8 } };
const {
a: hoge,
c: { x: fuga },
} = obj;
console.log(hoge); // output => 1
console.log(fuga); // output => 4
場面に応じて、名前の意味を変えたほうが良いときや、
別々の field がたまたま同じ名前の field を持ってて重複回避したいときなどに便利。
ECMAScript 2015 のオブジェクト初期化子
変数をそのままオブジェクトに突っ込むと、変数名が key になって、変数の値が value になるやつ。
MDN
に書いてある。
const hoge = "fuga";
const piyo = { hoge };
// piyo => {hoge: "fuga"}
どこでも使うんだけど、強いて言うなら、
タグごとの記事数のカウント情報を作るときに使う ↓。
const allTags = [`hoge`, `hoge`, `fuga`, `hoge`, `fuga`, `piyo`];
const tagInfos = Array.from(new Set(allTagNames)).map((name) => {
const count = allTags.filter((n) => n === name).length;
return { name, count };
});
// tagInfos => [
// {name: "hoge", count: 3},
// {name: "fuga", count: 2},
// {name: "piyo", count: 1},
// ]
URL 操作
Gatsby は web サイト生成ツールなので、当然、URL の文字列をいじる機会が多い。
URL の予約文字のエスケープ
URL に使える文字は
RFC3986
とやらで決まっているらしい。
文字の種類としては、『reserved』と『unreserved』と『記載なしの文字』の 3 種類がある。
『reserved』と『記載なしの文字』はエスケープする必要がある。
- 『reserved』:
#
とか$
とか - 『unreserved』:
-
とか.
とか - 『記載なしの文字』: スペースとか
%
とか ASCII 制御文字とか多バイト文字(U+80
以降)とか
RFC3986 に加えて、Gatsby の
createPage()
の path
の特性も知る必要がある。
こいつは多バイト文字だけエスケープ(パーセントエンコーディング)してくれる。
(というより、「事前にパーセントエンコーディングはするな」って書いてある。)
この 2 点を考えると、 encodeURI()
とかは使えない。
↓ の仕様を満たす関数が必要となる。
- 『reserved』は削除
- 『unreserved』は何もしない。
- 『記載なしの文字』はエスケープする(
-
に置換する)。- ただし、多バイト文字はエスケープしない。
↑ にピッタリはまるライブラリが見つからなかったので、
自分で作った ↓。
const kebabCase = (str) => {
if (!str?.replace) throw new Error(`Must have a \`replace\` method.`);
if (!str?.toLowerCase) throw new Error(`Must have a \`toLowerCase\` method.`);
return str
.replace(/[ !"#$%&'()*+,/:;<=>?@\[\\\]^`{|}]/g, `-`)
.replace(/[\u0000-\u001F\u007F]/g, ``)
.replace(/^-+|-+$/g, ``)
.replace(/-{2,}/g, `-`)
.toLowerCase();
};
↑ の仕様以外に、連続するエスケープを 1 つにまとめたり、先頭と末尾の -
を削除したり、小文字に統一したりもしてる。
ちなみに、「予約文字(非多バイト文字)をエスケープしないのはバグでは ❓」って
issue
を Gatsby に出したけど、「ユーザが自分で実装すべきじゃね ❓」って言われてクローズされちゃった 😭。
スラッシュの処理
末尾と先頭の /
の処理は何も考えてないとバグりまくる。
↓ はやりがち。
const path1 = `/post`;
// 別のファイルとかで別のパスを作る
const path2 = `/2022/01/`;
// また別のファイルで結合する
const fullpath = `${path1}/${path2}`;
// output => "/post//2022/01/"
// スラッシュがダブる → バグ
なので、 /
をいい感じにしてくれる便利関数を作っておくと良い。
ネット上のいろんなコードを参考にして作ったのが
これ ↓。
const slugify = (...dirs) => {
const validDirs = dirs.filter((dir) => dir?.toString);
const url = validDirs.map((dir) => kebabCase(dir.toString())).join(`/`);
return `/${url}/`.replace(/\/\/+/g, `/`);
};
slugify(`hoge/`, `/fuga`);
// output => "/hoge/fuga/"
フルパスの生成
JavaScript 標準の URL
を使うと便利かも。
const url = new URL(`https://example.com/`);
url.pathname = `/hoge/fuga/`;
const fullPath = `${url.origin}${url.pathname}`;
// output => "https://example.com/hoge/fuga/"
pathname
の先頭の /
は無視してくれる。
が、連続した /
や末尾の /
は自分でケアする必要があるので注意。
https://example.com/
の部分は siteMetadata
から引っ張ってきて、
pathname
の部分は slugify()
を通したものを入れると良いかな。
null や undefined や空配列の判断
GraphQL の挙動がぶっちゃけよく分かってなくて、
null
だったり、 undefined
だったり、空配列が返ってきたりする。
全部まとめて ↓ のように判断できる。
const edges = result.data.allMarkdownPost.edges;
if (!edges?.length) {
// エラー処理
}
スプレッド構文
配列とかオブジェクトをいい感じに展開してくれるアレ。
配列のスプレッド構文
指定の要素ごとに記事数をカウントした配列があるとして、
それの最大値を取るには ↓ のようにする。
const counts = [20, 1, 300];
const max = Math.max(...counts);
// output => 300
Math.max()
は配列のまま投げても NaN
しか返ってこない。
ので、スプレッド構文で展開して渡す必要がある。
先述の slugify()
を使うときもスプレッド構文を使う ↓。
const relativePath = `hoge/fuga/piyo.md`;
const { dir, name } = path.parse(relativePath);
const slug = slugify(`base`, `post`, ...dir.split(`/`), name);
// output => "/base/post/hoge/fuga/piyo/"
オブジェクトのスプレッド構文
オブジェクトを展開すると、2 つのオブジェクトを簡単に結合できたりする ↓。
const hoge = { a: 1, b: 2, c: 3 };
const fuga = { ...hoge, z: 99 };
// output => { a: 1, b: 2, c: 3, z: 99 }
createNode()
のときによく使う。
自分流の markdown の記事用の node を作るときは ↓ のように fieldData
を展開している。
const fieldData = {
canonicalUrl: node.frontmatter?.canonicalUrl || ``,
date: node.frontmatter?.date ? node.frontmatter.date : "2999-01-01 00:00",
heroImage: node.frontmatter?.heroImage,
slug,
tags: modifiedTags,
title: node.frontmatter.title ? node.frontmatter.title : subdirs.slice(-1),
};
createNode({
...fieldData,
// Required fields
id: createNodeId(`${node.id} >>> MarkdownPost`),
parent: node.id,
children: [],
internal: {
type: `MarkdownPost`,
contentDigest: createContentDigest(fieldData),
content: JSON.stringify(fieldData),
description: `MarkdownPost`,
},
});
時刻操作
Date
の API を使いこなすと良い。
Date.prototype.getMonth()
や Date.prototype.getFullYear()
は言うまでもないので割愛。
英語圏用に Jan
とか Feb
とかの文字列がほしければ
Date.prototype.toLocaleString()
を使って ↓ のようにする。
const yearMonth = new Date().toLocaleString(`en-US`, {
year: `numeric`,
month: `short`,
});
// output => "Feb 2023"
日本語にしたいときは ↓。
const yearMonth = new Date().toLocaleString(`ja-JA`, {
year: `numeric`,
month: `short`,
});
// output => "2023年2月"