4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Routify (Svelte Router) をコードリーディングして完全に理解する

Last updated at Posted at 2020-12-24

この記事はAteam Brides Inc. Advent Calendar 2020の25日目の記事です。

成り行き

ワイ 「今年もアドベントカレンダーの季節がやってきたなぁ」
@hyshr 「スペシャリストとして強い記事でトリを飾れ」(指令)
ワイ 「お、おう。そか・・・。何書こ」
@oekazumaRoutifyのコンパイラをコードリーディングして!」

なお、フロントエンド雑魚ワイ。

前提

  • 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スクリプトはこちら

package.json
  "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

package.json
  "bin": "./plugins/cli.js",

とあるように cli.js から追いかけていこう。

plugins/cli.js
program
  .command('run', { isDefault: true })
    // 省略
    require('../lib/services/interface').start(options)
  })

ということで、interface.js を見ていくとどうやら Builder() を呼び出していて

lib/serivices/interface.js
  const build = Builder(options, false, metaParser)

Builderの中で、更にrunMiddlewareを呼び出す。
どうやらここが本丸っぽい。

lib/serivices/interface.js
    const payload = await runMiddleware({ options, metaParser, state })

Routifyのミドルウェア

コンパイラの役割を機能ごとに分解して、そのパイプ処理で最終成果物を作っている。
そして、その単機能をミドルウェアとして個別に定義しているので、機能追加とかも柔軟に行えるってこと1
定義してあるミドルウェアはこれら。

lib/serivices/middlewareRunner.js
  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
  }

大雑把に各処理を説明すると

  1. 初期化して
  2. ファイルツリー作って
  3. _ で始まる(読み込み専用コンポーネントなどの)ディレクトリは無視して
  4. メタデータを適用して
  5. ネストした情報に対して適切なメタデータを選択して
  6. IDを付与するためにimportのpathを改ざんして
  7. ファイルIDを作り
  8. ソートして
  9. ID付きのファイルをコンポーネントに読み込んで
  10. ルーターに食わせるためのノードツリーを生成し
  11. ファイルに実体化した後
  12. ルート情報を書き出す

といったプロセスを流して、rollupでバンドルする(そしてその際にApp.svelteをSvelteに食わせる)って流れを実現している流れだと思われる。
そしてApp.svelteはRouterをimportしているので、Routerがurlディスパッチャを使ってレンダリングしているという形になる。

では、この中でもRoutifyの特徴を実現している部分に着目して見ていこう。

メタデータ

メタデータとは、ページやレイアウト単位で適用されたコンポーネントに埋め込まれたデータを、他のコンポーネントで利用するためのもの。

まず、メタデータのパーズ処理だけどかなり強引に実装してあった。

lib/service/metaParser.js
    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コンポーネントの関係性が出力されている。
また、それらの関係性を元に新たな treeroutes というオブジェクトが生成されている。
生成するのは buildRoutes.js が担っている。
こちらもまた、プラグインのパイプ処理によりパス情報を加工した結果となっている。

runtime/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 コンポーネントに与えられている。

App.svelte
<Router {routes} />

Routerは与えられた routes を使って、navigatorを呼び、更にurlToRouteを呼び出している。
ようやくURLディスパッチャの素顔が拝めた。

runtime/utils/urlToRoute.js
    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大会の定期開催など、アウトプットの場を増やして事業・組織・個人の成長を目指しています。
気になった筆者がいれば是非フォローしてください。きっと継続的にアウトプットしてくれます。

それでは、メリークリスマス!

  1. でも、前の処理結果を期待して作ってあるので、前後のI/Fが合わないとコケる作り。バンドラーのプラグインとかで素人(僕)がハマりがちな構成のつらさも感じる。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?