📚
Elm-pages in Action
Elm-pages in Action
...あるいは、elm-pagesの概念的ポイント解説
By ymtszw
@Elm-jp 2023 (2023/05/20) [1]
前回?までのあらすじ
- 昨年、elm-pages (v2)とHeadless CMSに入門し、好印象を抱いた
- せっかくなのでpersonal websiteとして成果物を整備し、その後継続的に記事更新したり、サイト改良したりしている
- ソース
- 昨年後半のElm Meetup (online)でコミュニティ製フレームワークの一つとして紹介
今回は…
- elm-pages v2の概念的なポイントを紹介
- elm-pages v3で新たに取り組まれているところと現状の共有
そもそもelm-pagesとは
- 基本的には静的サイトジェネレータ(SSG)である
- 主要な部分はすべてElmで記述できる
- 以下、v2のAPIの概念的なポイントを紹介
Build-timeとRun-time, 2つのElm App実行
- SSGである以上、事前にコンテンツを静的なHTMLとして生成し、配信したい
- しかし、Elmはクライアントでの実行時にコンテンツが動的生成されるSPAに特化した言語だったはず…
=> elm-pagesは、2つのタイミングでのElm App実行によってこのギャップを埋める
Build-time App
-
elm-pages build
を実行したとき、elm-pages CLIはまずボイラープレートコード生成を行う- elm-pagesはFile-based Routingを採用
- ファイル名(=モジュール名)規則を元に対応するroutingコード等が生成され、root appの
main
関数に結線するあたりは自動
module Page.Index exposing (..)
module Page.Articles.ArticleId_ exposing (..)
- 生成されたElm Appは実際にpre-rendering機構によって実行され、結果としてのHTMLがrouteごとに得られる
- これらのHTMLがSSGとしての第一の成果物
- 適当なWebサーバから配信すれば、クローラーフレンドリーなWebサイトとして機能する
- JavaScriptが無効化されていても、prerenderされたwebサイトは正常に表示される
Run-time App
- 一方、生成されたElm Appそれ自体も、第二の成果物となる
- 第二の成果物は、以下の要素から成る:
- みなさんが想像するElm SPAそのもの
- routeごとに事前生成されたHTMLを表示後、上記のElm SPAをクライアントwebブラウザで実行し、以後の処理をSPAに移譲するためのグルーコード
- この構成によって、静的サイトの事前生成も、webブラウザでのサイト読み込み後の動作もいずれもElmで記述する、という世界観が成立する
- …さて御存知の通り、Elm 0.19のVDom実装にはいわゆるhydration機能(VDomを元にSSRされた結果から、ブラウザ内でVDomを再構築する機能)がない
=> それではどのようにして、「以後の処理をSPAに委譲」するのか?
決定論的rendering
- 予め結論を述べると、意外に特別なことはやってない
-
DOMContentLoaded
イベント契機で第二の成果物であるElm Appを起動しているだけ
// 成果物HTMLで実質的に実行されるPromise
const appPromise = new Promise(function(a){
document.addEventListener("DOMContentLoaded", () => {
a(loadContentAndInitializeApp())
})
});
-
- Build-timeのpre-rendering
-
- ブラウザ内でのclient-side rendering
=> この2つが(少なくともrender完了直後のフレームにおいて)寸分違わず一致するならば、第一の成果物由来のHTML表示そのままに、第二の成果物由来のSPAによる処理に間断なく移行できる、という理屈
- 「寸分違わず一致」させるために、elm-pagesはこれら二種類のrendering結果が決定論的に定まるよう設計されている
- 具体的には、
- ページがrendering時に依存するデータの
DataSource
による抽象化 - Flagsの分離
- ページがrendering時に依存するデータの
- (注: 多分ほかにも工夫はあるけど、概念的にポイントとなるのはこのへんだと理解している)
DataSource
-
Build-timeに解決されるデータ
- 由来はCMS等の外部APIでもいいし、ローカルファイルでもいい
- elm-pagesの各ページモジュールは、
init
時には基本的にDataSource
にのみ依存できる - ポイントは、解決された
DataSource
の内容は、JSONとしてシリアライズされてHTML(第一の成果物)に埋め込まれること
- Run-timeに実行されたElm App(第二の成果物)も、Build-timeから持ち越された
DataSource
をそのまま読み込んでページモジュールをinit
する - これによって両者の評価結果が一致する仕組み
- たとえば、CMS APIの返す値がbuild-timeとrun-timeで変化していても影響を受けない
Flagsの分離
- 通常、Elm App起動時のもう一つの依存データはFlags
- elm-pagesでは、
type Flags
= BrowserFlags Json.Decode.Value
| PreRenderFlags
- このように2つのElm Appが受け取るFlagsが分離される
- 見て分かる通り、pre-render時にはFlagsは存在できない!
- 例えば、Elm App起動時のブラウザの画面サイズや現在時刻を取得する、といったことはpre-render時にはできないよう型で保証されている
- 2つのElm Appは、どちらの起動タイミングでも最初はpre-render状態(Flagsなし)で起動する
- Webブラウザ上で起動したとき、
BrowserFlags Json.Decode.Value
がElm App起動後の処理で遅れて利用可能となる
elm-pages v2のポイントまとめ
- Built-timeとRun-time, 同じElm Appを2つのタイミングで実行
- かつ両者の結果が一致するよう設計することで、できないはずだったSSG→SPA委譲を可能にする
- そのための工夫
DataSource
- Flagsの分離
elm-pages v3では何が起きようとしているか?
- v2までで、いわゆるJAMStackにおけるSSG→SPAという部分についてはElmだけで実現可能になっている
- 事前にページ生成して検索インデックスに乗るようにし、ページ表示後はCSRで動的コンテンツ提供
- が、御存知の通りこの構成には中間地点が欲しくなる
- 特に事前に生成したいページが大量にあったり、頻繁に更新されるようなwebサイト/webサービス
- 最たるものはECサイトや、ユーザ生成コンテンツからなるサービスなど
- その中間地点を満たすのがSSRの諸形態
- 特にNext.jsのISG; Incremental Static GenerationやISR; Incremental Static Regeneration, On-demand ISRあたり
- 参考
- アプローチ
- ISG: リクエストがあったときに初めて静的生成を行い、それをエッジキャッシュに載せる
- ISR:
stale-while-revalidate
戦略で、キャッシュデータを高速に表示しつつ再生成も行う - On-Demand ISR: 更に、イベントドリブンな適時再生成にも対応して、stale表示を最小化する
- elm-pages v3では、端的に言うとISG相当のアプローチに対応しようとしている
- Server-rendered route
- リファレンス実装はNetlify Functionsを使用
- Adapter層を導入することで、他のプラットフォームにも対応
v3 status
- ほかにも、webサイトで頻出の要求であるフォーム送信機能もサポートするため、設計・実装中
- 更に、
DataSource
等の内部実装を効率化するためにLamdera compilerを採用 - ...and more!
=> という状況のため、v3-betaとそのstarter repoはまだまだ流動的。一方v2のAPIはそこそこ 「枯れている」と言っていいくらいに整っているので、23年5月現在ならv2でまずはデビューしてみてください
あとがき
- elm-pagesの中心概念はどっかで自分の言葉で書いておきたいと思ったのでこのような内容になった
- ホントはタイトルの"in action"にもっと寄せて、
- v2でのパフォーマンスチューニングとか、
- 自前サイトでの具体的なコードの話とか、
- いくつか裏技小技とか、
- その辺も書きたいのだが、また別の機会に
- 著者紹介(tokyo.ex#20のスライド)
Q. elm-pagesはpre-render時にどのタイミングで生成されたHTMLを確定するの?
A. いい質問ですね。 個人的にあまり深追いしていなかった部分です。
- 例えばページのsubscription関数で
onAnimationFrame
を使ったりして、次フレームからいきなりページ内容が連続的に変化するような実装をした場合、pre-renderとbrowser-renderの2つのアプリの一致点を取りにくいんではないか。
- pre-renderに使ってるライブラリがよしなにやってくれてるのかもしれない。
- 最初のフレームで止めるとか?そんなことできるんだろうか
- v1のときはprerender.ioを使ってたような記憶があるけど今どうなってるんだろう。
- 読者への課題とします。個人的にも追って調査します。
Discussion