この記事はAteam Brides Inc. Advent Calendar 2020の25日目の記事です。
成り行き
ワイ 「今年もアドベントカレンダーの季節がやってきたなぁ」
@hyshr 「スペシャリストとして強い記事でトリを飾れ」(指令)
ワイ 「お、おう。そか・・・。何書こ」
@oekazuma 「Routifyのコンパイラをコードリーディングして!」
なお、フロントエンド雑魚ワイ。
前提
- Routifyは執筆時点でのmasterブランチ(ver 2.999.2)を対象とする
- Routify単体ではなく普通に利用した場合のテンプレートから探っていく
- ビルドする過程を追うため、ホットリロードなどの機能には立ち入らない
- Svelteのコンパイラにも踏み入らない
- 解説記事があるのでそちらを参照
Routifyとは何か?
いわゆるRouter。
Svelteはテンプレートエンジンのようなもので、記載されているものをコンパイルしてhtml/js/css/を生成するものなので、URLに従って適切なページを表示するRouterを搭載していない。
なので、SapperとかRoutifyとかのRouterを利用してSPAを作成する。
最初に結論だけ
- Router.svelteが必要なコンポーネントをロードした状態にしている
- navigator.jsを通してURLにあったコンポーネントをレンダリングする仕組み
- ルート情報とコンポーネントの関係はミドルウェアと称されるフィルタ群でまとめ上げられている
- App.svelteがRouter.svelteを呼んでいるので、App.svelteをコンパイルすれば適切にディスパッチされるSPAとなる
コードリーディング開始
テンプレートのpackage.json
Rollupテンプレートが提供しているnpmスクリプトはこちら
"scripts": {
"dev": "run-p routify nollup",
"dev:ssr": "run-p routify rollup",
"build": "run-s build:*",
"build:app": "routify -b && rollup -c",
"build:static": "spank",
"serve": "spassr --ssr",
"rollup": "rollup -cw",
"nollup": "nollup -c",
"routify": "routify"
},
READMEに書いてあるものと違うけど(2020/12/22現在)、ドキュメントが追いついてないだけ。
このうち、今回着目していくのは build:app
。
Routifyがどうやってコンパイルしているのかを端的に読めるので。
RoutifyのCLI
"bin": "./plugins/cli.js",
とあるように cli.js
から追いかけていこう。
program
.command('run', { isDefault: true })
// 省略
require('../lib/services/interface').start(options)
})
ということで、interface.js
を見ていくとどうやら Builder()
を呼び出していて
const build = Builder(options, false, metaParser)
Builderの中で、更にrunMiddlewareを呼び出す。
どうやらここが本丸っぽい。
const payload = await runMiddleware({ options, metaParser, state })
Routifyのミドルウェア
コンパイラの役割を機能ごとに分解して、そのパイプ処理で最終成果物を作っている。
そして、その単機能をミドルウェアとして個別に定義しているので、機能追加とかも柔軟に行えるってこと1。
定義してあるミドルウェアはこれら。
const _middlewares = {
initial,
generateFileTree, // => children
removeUnderscoredDirs, // _private => false
defineFiles, // file => ({ isLayout, isReset, isIndex, isFallback })
removeNonSvelteFiles, // remove _file.svelte, keep dirs, layouts & resets
applyMetaToFiles,
applyMetaToTree,
addPath, // ... => ({ path, ... })
addId, // ... => ({ id, ... })
defaultSort,
attachComponent, // { component }; { path } for layouts
template,
createBundles,
writeUrlIndex
}
大雑把に各処理を説明すると
- 初期化して
- ファイルツリー作って
-
_
で始まる(読み込み専用コンポーネントなどの)ディレクトリは無視して - メタデータを適用して
- ネストした情報に対して適切なメタデータを選択して
- IDを付与するためにimportのpathを改ざんして
- ファイルIDを作り
- ソートして
- ID付きのファイルをコンポーネントに読み込んで
- ルーターに食わせるためのノードツリーを生成し
- ファイルに実体化した後
- ルート情報を書き出す
といったプロセスを流して、rollupでバンドルする(そしてその際にApp.svelteをSvelteに食わせる)って流れを実現している流れだと思われる。
そしてApp.svelteはRouterをimportしているので、Routerがurlディスパッチャを使ってレンダリングしているという形になる。
では、この中でもRoutifyの特徴を実現している部分に着目して見ていこう。
メタデータ
メタデータとは、ページやレイアウト単位で適用されたコンポーネントに埋め込まれたデータを、他のコンポーネントで利用するためのもの。
まず、メタデータのパーズ処理だけどかなり強引に実装してあった。
const re = RegExp(/\<\!\-\- *routify:options +((.|[\r\n])+?) *\-\-\>/, 'g')
ここで得られたメタデータを applyMetaToTree()
で各コンポーネントに適用している。
ID作り
これはrollup-pluginutilsへ処理を委譲していたので、rollupで標準的につけられるモジュールのIDが付与されているようだ。
これがsvelteでコンポーネントにつけられるASTのIDと同じものかどうかまでは検証できていない。
読み始めたときにはスコープ用のIDと思っていたけど、そうじゃなくてビルドしたときの成果物ファイルを作るためのIDだった様子。
あまりRoutifyの特徴とは関係なかった。
ルート情報の書き出し
最終的に作られるのは .routify
以下のファイル群になる。
これらの内 route.js
がキモで、この中にURLパスとsvelteコンポーネントの関係性が出力されている。
また、それらの関係性を元に新たな tree
と routes
というオブジェクトが生成されている。
生成するのは buildRoutes.js
が担っている。
こちらもまた、プラグインのパイプ処理によりパス情報を加工した結果となっている。
const order = [
// all
"restoreDefaults",
// pages
"setParamKeys", //pages only
"setRegex", //pages only
"setShortPath", //pages only
"setRank", //pages only
"assignLayout", //pages only,
// all
"setPrototype",
"addMetaChildren",
"assignRelations", //all (except meta components?)
"setIsIndexable", //all
"assignIndex", //all
"assignAPI", //all
// routes
"createFlatList"
]
となっている。
一つ一つ取り上げるほどのボリュームのある内容ではないが、結構重要なことを行っている。
- setRegex: [id].svelteなどのパラメタ付きコンポーネントから、マッチするURLを特定するための正規表現(後述)を作る
- assignRelations: HelperのgetConcestorの元情報を付加
など、我々が触れる部分に近い機能のいくつかは、この過程で実現されている。
そうしてルート情報を掘り返しながら作った情報を元に、assignAPI
で各コンポーネントから呼べる状態にしてある。
URLディスパッチャ(Router.svelte→navigator.js→urlToRoute.js)
上記の様に作られたルート情報は、App.svelte
で呼び出されている Router
コンポーネントに与えられている。
<Router {routes} />
Routerは与えられた routes
を使って、navigator
を呼び、更にurlToRoute
を呼び出している。
ようやくURLディスパッチャの素顔が拝めた。
const route =
// find a route with a matching name
routes.find(route => url === route.meta.name) ||
// or a matching path
routes.find(route => url.match(route.regex))
今までの過程で作られたパスとコンポーネントの情報ツリーがあるからこそ、ここでURLに一致するか正規表現で一致するか、というシンプルなロジックのみで該当するコンポーネントを取得できる。
最終ビルド
App.svelteがRouter.svelteを呼び、その中でディスパッチャを通して各コンポーネントを動的に呼べるためにロードしている状態まで作れた。
あとはこれをSvelteでコンパイルすれば、長大なコンポーネント群を一つにまとめ上げたSPAの完成となる。
アドベントカレンダー最終日
Ateam Brides Inc. Advent Calendar 2020をご覧いただきありがとうございました。
今年は、多様な職種・立場で働くエイチームブライズの仲間達が、それぞれの知見や経験を発信してくれました。
世間でアウトプットの重要性がブームになったのがおそらく2年ほど前のこと。
エイチームブライズではアドベントカレンダーだけでなく、社内LT大会の定期開催など、アウトプットの場を増やして事業・組織・個人の成長を目指しています。
気になった筆者がいれば是非フォローしてください。きっと継続的にアウトプットしてくれます。
それでは、メリークリスマス!
-
でも、前の処理結果を期待して作ってあるので、前後のI/Fが合わないとコケる作り。バンドラーのプラグインとかで素人(僕)がハマりがちな構成のつらさも感じる。 ↩