Gatsby.jsでブログを始めたのでページ生成の仕組みを学ぶ
July 01, 2020
掲題の通り、Gatsby.jsを利用したブログを作り始めた。 技術的なことはたまにQiitaに書いていたのだが、それ以外のことを書きたくなった時のために用意したかったことと、 ドメインを取得してずっと放置していたのでそろそろ使ってあげようという気持ちがあった。
https://www.staticgen.com/ を眺めながら、静的サイトジェネレータを探す中、 Hugo、Nuxt.jsは触ったことがあったので、今回は経験のないGatsby.jsを選ぶことにした。
個人的な決め手としては、データ取得にGraphQLが採用されており、別途APIサーバーを立てるわけではなくビルド時に内部で生成されるというような静的サイトならではのGraphQLの利用方法や仕組みに興味がそそられたことと、 Googleが提唱するPRPLパターンに則っており、考え方やこれらを支える技術を学ぶことでWebパフォーマンスの高速化に関する知見を深められるのではと考えたことが採用理由となった。
Gatsby.jsとは
https://www.gatsbyjs.com/
https://github.com/gatsbyjs/gatsby
GatsbyはReactベースの高速なページ作成に特化したOSSフレームワーク。 モダンなWebフロントエンド技術を利用したプロジェクトを比較的簡単に構築でき、SPAとして動作するため高速な画面遷移を行う一方、ローカルファイルからデータを取得し静的HTML、JavaScript、CSSの生成を行うことでSPAのデメリットである初回読み込みの遅さを補い、高速なページの作成を実現している。
静的サイトジェネレータとしての印象が強いが、
Go Beyond Static Websites. Get all the benefits of static websites with none of the limitations. Gatsby sites are fully functional React apps so you can create high-quality, dynamic web apps, from blogs to e-commerce sites to user dashboards.
とあるように、静的サイトだけではなくReactの機能を利用して動的なサイトの作成もできるとの紹介がされている。
GatsbyがReact DOM server-side APIを利用して静的なHTMLを生成していることや、静的HTMLをReact Hydrationを介してクライアントのJavaScriptで拡張できることにより、 静的・動的のメリットをあわせ持つページが作成でき、fetchやaxiosを利用した動的なデータ要求もできる。
https://www.gatsbyjs.com/blog/2018-10-15-beyond-static-intro/ https://www.gatsbyjs.com/docs/data-fetching/
Gatsby’s blog starter
https://github.com/gatsbyjs/gatsby-starter-blog
公式が提供している一番メジャーなブログスターター。
できるだけ余計な依存は入れたくない気持ちがありつつも、ある程度動くものを触りながら学習したい気持ちが勝ち、利用することにした。
Markdownでの記事作成や、画像の最適化、RSSのサポート、GoogleAnalyticsのサポート、シンタックスハイライト、タイポグラフィプラグインなどの機能が含まれている。
GraphQL API
https://www.gatsbyjs.com/docs/graphql-api/
Gatsbyの特徴的な点として様々なデータソース(MarkDownやWordPress、Contentful、YAML、JSON etc..)から取得したデータをGraphQLを経由して統一された方法で取得できるというものがある。 BFFのような役割を担ってくれるため、利用する側からはスキーマ定義とクエリを書けばよく、その先のデータソースを意識せずとも良くなる。
プロジェクトを作成し、
$ gatsby develop
を実行すると、ビルド時にスキーマが自動的に推測されて作成され、 ホットリロードに対応した開発用サーバーが立ち上がると同時に、 https://localhost:8000/___graphql でGraphiQLが利用できるようになる。
※ 「gatsby develop」 と 「gatsby build」 の違いやビルドプロセスについては、以下で紹介されている。
https://www.gatsbyjs.com/docs/overview-of-the-gatsby-build-process/
You can now view ntsk in the browser.
⠀
http://localhost:8000/
⠀
View GraphiQL, an in-browser IDE, to explore your site's data and schema
⠀
http://localhost:8000/___graphql
⠀
Note that the development build is not optimized.
To create a production build, use gatsby build
以下は、2記事書いた状態で、全記事をクエリしてみた例。
GraphiQLのExplorerボタンを押すと利用できるプロパティが表示されるが、
前述の通り、自動で推測されて作成されている。
https://www.gatsbyjs.com/docs/schema-generation/ によると graphql-compose ライブラリを利用し、ユーザー定義のスキーマ情報とノードの形状からスキーマ推論を行ったものを組み合わせて生成されているとのこと。
また、gatsby-transformer-remark というプラグインが、 Markdownで記述した記事をパースしてGraphQLフィールドに変換を行っており、allMarkdownRemark や MarkDownRemark プロパティを利用可能にしているようだ。
Apolloやgraphql-rubyは利用したことがあったが、スキーマ定義も自動生成されるというのは初めてだったので勉強になった。 graphql-composeについても、今度触ってみようと思う。
Gatsby Node APIs
https://www.gatsbyjs.com/docs/node-apis/ https://www.gatsbyjs.com/docs/api-files-gatsby-node/
Node APIでは、GraphQLのデータ構造の定義、スキーマ生成や、ページの動的生成など、サイトのデータ制御のためのAPIが提供されており、 gatsby-node.js ファイル上で、ライフサイクルに応じて処理を加えたいメソッドを呼び出して記述するとビルド時に反映されて実行される。
前述の gatsby-transformer-remark はこのAPIの setFieldsOnGraphQLNodeType でスキーマ作成される際に利用されている。
https://www.gatsbyjs.com/docs/node-apis/#setFieldsOnGraphQLNodeType
ドキュメントで各APIの説明がされているが、一度に全部は把握しきれないので今回は gatsby-starter-blog が加えている処理を追ってみる。
createPages
https://www.gatsbyjs.com/docs/node-apis/#createPages
名前の通りページを作成のためのメソッドで、ソースの初期化やGraphQLのスキーマ生成のあとに呼び出される。 クエリでデータを取得してページの作成を行う。
gatsby-starter-blog ではデフォルトで以下のように呼び出されている。
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions
// Define a template for blog post
const blogPost = path.resolve(`./src/templates/blog-post.js`)
// Get all markdown blog posts sorted by date
const result = await graphql(
`
{
allMarkdownRemark(
sort: { fields: [frontmatter___date], order: DESC }
limit: 1000
) {
nodes {
fields {
slug
}
frontmatter {
title
}
}
}
}
`
)
if (result.errors) {
reporter.panicOnBuild(
`There was an error loading your blog posts`,
result.errors
)
return
}
const posts = result.data.allMarkdownRemark.nodes
// Create blog posts pages
// But only if there's at least one markdown file found at "content/blog" (defined in gatsby-config.js)
// `context` is available in the template as a prop and as a variable in GraphQL
if (posts.length > 0) {
posts.forEach((post, index) => {
const previous = index === posts.length - 1 ? null : posts[index + 1]
const next = index === 0 ? null : posts[index - 1]
createPage({
path: post.fields.slug,
component: blogPost,
context: {
slug: post.fields.slug,
previous,
next,
},
})
})
}
}
ラムダ式の引数で、graphql, actions, reporter の3つの引数が渡ってきている。 これは、Node API Helper と呼ばれ、各Node APIから必要な機能が提供されたオブジェクトが渡ってくる。 graphql はクエリを利用したデータ取得、 actions はサイトの状態操作のための機能のコレクション、 reporter はログ出力用のデバッグ機能を提供している。
各APIで共通のものもあれば、API固有のものも存在し、graphqlに関しては、createPages で独自で用意されているヘルパーとなる。
これらを利用して、非同期でデータを取得し、createPageアクションを利用してページの作成を行っている。
onCreateNode
https://www.gatsbyjs.com/docs/node-apis/#onCreateNode
新しいNodeが作成された際に呼び出されるメソッド。 他のプラグインによって生成されたNodeを拡張したい場合には、このメソッドにて実装する。
NodeとはGatsbyのデータシステムでGatsbyに追加されるすべてのデータはノードを利用してモデル化されている。 https://www.gatsbyjs.com/docs/node-interface/ これにより、複数のデータソースの一元管理を行い、GraphQLのスキーマに変換されている。
gatsby-starter-blog では、MarkdownRemark へ slug フィールドを拡張している。
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({ node, getNode })
createNodeField({
name: `slug`,
node,
value,
})
}
}
createSchemaCustomization
https://www.gatsbyjs.com/docs/node-apis/#createSchemaCustomization
GraphQLのスキーマをカスタマイズするメソッド。 createTypes, createFieldExtension, addThirdPartySchema のアクションを利用してカスタマイズを行うことが可能。
gatsby-starter-blog では、コメントに記載されている通り、 gatsby-config.js で記述されているsiteMetadataが削除されても利用できるよう明示的に定義している他、 記事がない場合でもエラーではなくnullが返るよう MarkdownRemark Frontmatter も明示的に定義しているようだ。
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
// Explicitly define the siteMetadata {} object
// This way those will always be defined even if removed from gatsby-config.js
// Also explicitly define the Markdown frontmatter
// This way the "MarkdownRemark" queries will return `null` even when no
// blog posts are stored inside "content/blog" instead of returning an error
createTypes(`
type SiteSiteMetadata {
author: Author
siteUrl: String
social: Social
}
type Author {
name: String
summary: String
}
type Social {
twitter: String
}
type MarkdownRemark implements Node {
frontmatter: Frontmatter
fields: Fields
}
type Frontmatter {
title: String
description: String
date: Date @dateformat
}
type Fields {
slug: String
}
`)
}
まとめ
データソースをNodeへ変換することでデータを統合、GraphQLで統一的なアクセスをしつつ、NodeAPIにてデータ制御とページ生成周りの機能が提供されているという大まかな流れについて理解でき、デバッグの勘どころなどが掴めたように思う。
細かなAPIの機能や、設計やスタイリングなどのプラクティスなど、まだまだ理解が必要なことも多いが、 ドキュメントにて各APIや概念・思想に関しても丁寧に説明されており、Webフロントエンドを専門としていない自分でも楽しく読み進めることができた。
Gatsby.jsは簡単!初心者向け!というな紹介がされることが多い中、
SSR、SSG、SPA、PWA、React、GraphQL、babel、webpack、など数々のフロントエンドの知識が必要な中で、
初心者向けということがありえるのかと当初思っていた。(初心者というのがどの層をターゲットとしているのかという議論はあるが…)
しかし、実際に触ってみると動かすまでに必要な作業の少なさや、学ぼうと思った時にドキュメントを通して思想などの学びも深められることから、フロントエンドの知識が無くてもある程度の他のフレームワークなどを触ったことのあるエンジニアやデザイナーであれば、おすすめできるフレームワークであるように感じた。