NTTテクノクロスの上原です。
業務では、社内情報のReact製自前キュレーションサイトの構築を担当しています。
この記事はNTTテクノクロスAdvent Calendar24日目の記事であり、社内の勉強会で発表した内容をQiita記事として書きなおしたものです。タイトルは釣りです。
(2018/12/28追記あり)
導入
記事を書いた理由
Gatsby.js(以降、Gatsbyと表記)はさまざまな高速化テクニックを用いた「爆速サイト生成」で有名なツールですが、そのリッチな機能性は、たとえばイントラ内サイト、業務システム開発、ツール開発などでも十分に活用できるものだと思い、その可能性を紹介するために書きました。
「静的サイトジェネレータ」って何?
いわゆる「静的サイトジェネレータ(Static Site Generator, SSG)」は、CMS(コンテンツ管理システム)の一種です。代表的なものには以下があります。
他にも多くが実装・公開されています。
Webサイトの公開・構築に良く使われるWordPressなどのCMSは、記事の「閲覧時」に動的にサイト内容を生成しますが、静的サイトジェネレータは、閲覧時ではなく「ビルド時」にHTMLやCSSなどをあらかじめ生成しておくことが特徴です。
一般的な利点
「静的サイトジェネレータ」の一般的な利点は以下のとおりです。
- Webサイトのコンテンツを、サーバの設定や実行なしでAWS S3やGitHub pagesに置ける。これにより
- アプリサーバやDBが落ちるといった事象によってサイト公開が停止することがない
- 処理負荷に強い
- 動的CMS(Wordpress等)やアプリサーバ処理、DB処理に起因する脆弱性は回避できる
- 背景: Wordpress(及びそのプラグイン)の脆弱性は頻繁に発見されるので対応が大変
- サーバの利用・運用コストを削減できる
- CDNと相性が良くスケールしやすい
- Gitでコンテンツ管理ができる
Gatsbyとは
Gatsbyは、松田優作&翔太親子のCMでおなじみの...ではなく、ここではReactベースの静的サイトジェネレータです。最新のフロントエンド技術を駆使し、高速に閲覧できるサイトを生成できることで有名です。
たとえば、Reactの公式サイトはGatsbyを使用したサイトですが、このサイトをDevToolsで観察しながら閲覧すると、ページをスクロールするのに応じてクリックしなくても通信が走ることがわかります。これは、Gatsbyのランタイムが、表示エリアにリンクがはいってきた時点でリンク先コンテンツをプリフェッチしメモリ中に読み込み、クリック時には瞬時に表示できるようにしているからです。このような高速化のための高度な工夫が各種行われています。
わたしの疑問
自分が当初Gatsbyについて理解できていなかったのは、Reactとの関係です。
静的サイトジェネレータというぐらいだから、GatsbyはReactコードをSSR(Server Side Rendering)のようにあらかじめレンダリングして、静的なHTMLを事前に生成するのかな、と思いました。だから、JSコードはビルド時のみに実行されて、閲覧時には実行されないのかな、と。
このように思った理由は、他の静的サイトジェネレータの動作からの類推で、たとえばRubyベースのJekyllなどは、ビルド時にテンプレートをRubyインタプリタで評価してHTMLを生成し、閲覧時には一切Rubyコードは実行されません(されたら静的にならない)。ならば同じく、Gatsbyでも「JSコードはビルド時のみに実行されて、閲覧時には実行されない」のかなと思うじゃないですか。
しかし、調べると、そうではありませんでした。
Gatsbyの特徴と動作
GatsbyはSSR的な静的HTML生成に加えて、それと連動する通常のReactアプリも生成します。
Create React AppやNext.jsと同様に、Reactで開発するSPAの自由度を完全に具備するものです。
詳しく見てみましょう。
Create React App(CRA)の動作
まず、Gatsbyの説明をする前に、Create React Appを使ったときのReactアプリの生成と動作の様子を見てみます。下図のように、ビルド時にcreate-react-app build
コマンドを実行、bundle.jsを生成し、それを読み込むindex.htmlと合わせてデプロイします。Reactアプリは、ブラウザ中で初めて実行されます。
一般的なSSR(Next.jsなど)の動作
次に、Next.jsなどを用いたSSR(Server Side Rendering)を見てみましょう。
ブラウザが初期ページを読み込むタイミングで、サーバ側のNode.jsでReactアプリが実行され、初期HTMLを生成(SSR)します。ブラウザがそれを読み込んで初期表示し、引き続きReactアプリを実行し、仮想DOMの更新や、SPAとしての実行にうまいこと繋げてくれます。必要があればReduxのステートの転送なども行なわれます。
Gatsbyの動作
さて、Gatsbyです。Gatsbyは、Reactアプリをビルド時に1回実行し、HTMLを生成します。HTMLを生成する動作はSSRと同様なのですが、サーバ上ではなく、ビルドマシン上で実行することが異なります。このHTMLをJSと共にデプロイし、ブラウザはそれを初期ページとして読み込み、SSRと同様にReactアプリの実行が再度なされ、仮想DOM更新、SPAとしての実行、Reduxステートなどが引き継がれます。
そして、ブラウザ内でのReactアプリとしての実行は通常と同じで、API呼び出しを実行しても良いし、redux-sagaの実行など、任意の動作が可能です1。ただし、プリフェッチやコード分割にも関わるので、ルーティングはGatsbyの仕組みに従った方が良いでしょう2。Gatsbyでは、ルーティングは内部的にアクセシビリティ(a11y)向上のために@reach/routerが使用されています。
Material UIなどもプラグインを使用して使用可能です(コード例、参考になるページ)
Gatsby動作をもう少し詳しく
ビルド時GraphQL
上記まででも、サイト作成には十分に便利だと思いますが、Gatsbyのもう一つの大きな特徴は、ビルド時のさまざまな処理(データ取得と変換、使用)を「ビルド時GraphQL」で統一的に行えることです。
静的サイトジェネレータとしての典型的な処理は、Markdown形式のテキスト情報を、ファイルシステムから読み込んで、GraphQL経由で取得し、Reactコンポーネント内で表示することです。
しかし、Gatsbyではそれを上記のようにdata source, data transformerという枠組みで一般化することで、多様な処理を統一的にかつ簡潔に記述することができます。
こんなこともできる
ビルド時に形成されるGraphQL DBの内容は、ブラウザ内での実行時にはアクセスできません。これはビルド時だけのものです。
ちなみにたとえば、実行時にまったく別のGraphQLサーバにアクセスすることができます。
「ビルド時GraphQL」の結果をブラウザ内のReactコンポーネントにも渡す
さてここで一つの疑問が浮かぶかもしれません。
ビルド時に形成されるGraphQL DBの内容は、閲覧時のブラウザ内のReactアプリからはアクセスできないとしたら、ビルド時に得られたGraphQLのクエリ結果の情報は、ブラウザ内のReactコンポーネントではどのように入手できるのでしょうか? それが取得できないかぎり、SPAとしてHTMLと同じ画面を再現することはできません。
生成物を見ると、Gatsbyはこの問題を解決するために以下のような処理をしているようです。
- ビルド時
- GraphQLクエリをビルド時実行
- クエリ結果を使ってReactアプリを静的HTMLにレンダリング
- このとき得られたGraphQLのクエリ結果はJSONで保存しておく。
- デプロイ時
- 上記で生成された静的HTMLをデプロイ
- 同時に、上で保存していたJSONも静的コンテンツとしてデプロイ
- ブラウザでの閲覧時
- 静的HTMLを初期表示
- 裏でReactアプリ実行、仮想DOMを再構築(SSRと同じ)
- 保存されたJSONを読み込み、同じ表示を再現する
まとめると、ビルド時に形成されたGarphQL DBの全体は必要ないので、「クエリの結果」のみをJSONとして合わせてデプロイし、ブラウザ内ではGraphQL DBクエリ結果取得の代りにJSON値を使うことで、同じ表示を再現するというわけです。
Gatsby Plugins
GraphQL DBを作成するために、種々のdata transformer,data sourceがプラグインとして利用可能です。
![output_751673bcbfe8ef280417264a4042c06d-5.png](https://qiita-image-store.s3.amazonaws.com/0/9979/812431ca-6ab0-f08c-6fa4-a81ec237ae36.png)コード例
以下に、GraphQLを使ったGatsbyコードの例を示します。
処理内容は、gatsby-source-wordpressを用いてWordPressからAPIでビルド時に記事をとって来て、アイキャッチ画像含めたリンクとして画面に嵌め込むというものです。
Wordpress APIでビルド時に取得された情報から、以下のGraphQL クエリで一連の記事情報を取り出します。
export default withRoot(withStyles(styles)(Top))
export const pageQuery = graphql`
query {
allWordpressPost {
edges {
node {
id
title
link
content
featured_media {
source_url
}
}
}
}
}
`
以下ではクエリ結果としての記事情報を、Reactコンポーネントの内容に組み込んでいます。
class BlogPosts extends React.Component<IProps> {
public render() {
const { classes, allWordpressPost } = this.props
return (
<div
className={classNames(classes.layout)}
style={{ marginTop: '1rem', marginBottom: '1rem' }}
>
<Grid container={true} spacing={40}>
{allWordpressPost.edges.map(edge => {
const content = edge.node.content ? edge.node.content : ''
const strippedContent = content.replace(/<(?:.|\n)*?>/gm, '')
return (
<Grid key={edge.node.id} item={true} xs={12} sm={6} md={4} lg={3}>
<ContentCard
imageUrl={edge.node.featured_media.source_url}
heading={edge.node.title}
targetUrl={edge.node.link}
>
<Typography component="p">{strippedContent}</Typography>
</ContentCard>
</Grid>
)
})}
</Grid>
</div>
)
}
}
Gatsbyビルドのようす
上記の準備の上、プラグインの設定もした上で、以下のようにGatsbyプロジェクトをビルドできます。ビルド中にWordpress APIにアクセスしていることがわかります。
% npm run build
> [email protected] build /Users/uehaj/work/201812/techhub-gatsby
> gatsby build
success open and validate gatsby-configs — 0.013 s
success load plugins — 0.270 s
success onPreInit — 4.173 s
success delete html and css files from previous builds — 0.063 s
success initialize cache — 0.006 s
success copy gatsby files — 0.710 s
success onPreBootstrap — 0.007 s
⠂ source and transform nodes -> wordpress__POST fetched : 12
⢀ source and transform nodes -> wordpress__PAGE fetched : 5
⠐ source and transform nodes -> wordpress__wp_media fetched : 38
⠁ source and transform nodes -> wordpress__wp_taxonomies fetched : 1
⠄ source and transform nodes -> wordpress__CATEGORY fetched : 4
⢀ source and transform nodes -> wordpress__TAG fetched : 13
⠈ source and transform nodes -> wordpress__wp_users fetched : 4
success source and transform nodes — 2.745 s
success building schema — 0.798
:
組込まれた表示例
http://demo.wp-api.org/サイトからWordPress API経由で記事を取得してReact画面に組み込んだ例は以下のとおり。アイキャッチ画像が設定されてないので寂しい…。
# まとめ「爆速サイト」が必要ない場合でも得られるGatsbyの利点
サイトの高速性や、CDNで大規模スケールさせることは、イントラ向けシステムやツール開発などでは必ずしも必要ではないかもしれません。しかし、仮にそれを除いたとしても、Gatsbyには以下の利点があります。
-
通常ならDBで保持する/手書き修正のところ、ビルド時にUIに組込める。たとえば、
- 入力フォームの「組織一覧」の選択肢を、ビルド時に他のWebサイトやAPI、CSVなどから取得し・最新化する
- インクリメンタルサーチの選択肢
- 「運営からのお知らせ」情報
- なんらかの巡回収集
- 運用環境からはセキュリティ上の理由でアクセス可能にさせたくない情報源から抽出した情報の組み込み
- 上記をGraphQLとプラグイン群を使って、極めてシンプルに書ける。
- データの入力やオーサリングをWordPressなどCMSにまかせ、表示をカスタム化することでシステム開発を単純化できる
- しかもPHPを書かずに !!
- この用途に特化したHeadless CMSというジャンルのプロダクトも出ている
- CD(Continuous Delivery,継続的デリバリ)と組合せると、有用性はさらにUP!
- 「記事をWordPressで公開したタイミングでwebhookを叩いてビルド、デプロイ」など
- 各種の便利なプラグインが使用できる
JAMSatckアーキテクチャ1実装としてのGatsby
ちなみに、Gatsbyのような静的サイト生成を活用したWebシステムアーキテクチャを「JAMSatckアーキテクチャ」と呼ぶそうです。下図はhttps://jamstack.org/より引用。
以下が噛み砕いたJAMStackべからず集です。
- Wordpressを使わない
- ブラウジング時のSSRを使わない
- モノリシックではない
利点としては、静的サイトジェネレータで得られるものすべてに加え、CDN、およびその付随機能を活用できるということです。サーバーレス時代にも向いたアーキテクチャと言えるでしょう。
おわりに
まとめますと、
- Gatsbyを「静的サイトジェネレータ」と呼んでしまうと、「動的サイト」は作れない、という印象をもってしまうかもしれないがそうではなく3、React SPAとしてのすべての機能を発揮できる、CRAと同種の存在でもある。
- CRAと同様に、babelやwebpackを呼び出す
- もちろん、Wordpress代替として爆速、CDNを駆使しスケールする、といった優れた性質をもっており、Gatsbyにとって「静的サイトをジェネレートすること」は主要な用途である。
- しかしながら、Gatsbyの有効性はそれに限られず、以下のような利点があり、着目したい。
- UI構築の一部をビルド時に移動
- ある種の「サーバレス」を推進
- 強力なGraphQLを駆使して、UIコード構築処理を(ビルド時に前倒しした上で)簡素化する。
- UI構築の一部をビルド時に移動
と言うことで、いかがでしょうか、Gatsbyの魅力が少しでも伝われば幸いです。
では、みなさん良いお年を‼️(ムースをつけた両手で髪の毛をかきあげながら)
2018/12/28追記
記事を書いた後に、Gatsbyチームの方がインタビューで以下のように発言されているのを見つけました。本稿「私の疑問」のところで抱いた疑問は、他にも感じる人もいたということですね。
Q: What is one thing that Gatsby is capable of doing that might surprise some people? — ctlee
A: Gatsby can be used to build fully dynamic sites, which surprises some people because of it’s label as a “static site generator”. It’s fully equipped to be a powerful alternative to create-react-app and other similar solutions with the addition of easy pre-rendering and perf baked in.
(拙訳)
- Q: Gatsbyができることで、他の人が驚くかもしれないことは何ですか?
- A: Gatsbyは動的サイトの開発にも使うことができるので、Gatsbyを単なる「静的サイトジェネレータ」と考えている人は驚くかもしれないね。create-react-app(や同種のツール)の完全な代替となるだけでなく、プリレンダリング等多くの機能が組み込まれているんだ。