JavaScript
Node.js
webpack
Svelte

Svelte と Webpack で動的ルーティングを実現する

はじめに

この文章は、Svelte 環境構築まとめ の続きです。環境構築の前提条件についてはそちらを参考にしてください。

現代の Web アプリでは、JavaScript も CSS もまとめてバンドルして一括ダウンロードというのが一般的な作り方です。しかしながら、100を超えるような画面を持つ大規模なアプリケーションだと、サイズが大きくなりすぎてむしろローディングに時間がかかってしまうという問題があります。

そこで、webpack にはチャンク分割(split chunks)というファイルを分割し遅延ローディングを可能にする仕組みが用意されています。

この文章では、Svelte と組み合わせることで、ファイルを単にチャンクに分割するだけでなく、動的ローディングを使ってビュー単位での動的なルーティングを実現します。

共有ライブラリのチャンク分割

まず、output.chunkFilename と optimization.splitsChunks を webpack.config.js に追加し共有ライブラリを vendor ファイルに分割します。

webpack.config.js
    output: {
        path: path.resolve(__dirname, '/dist'),
        filename: '[name].js',
        chunkFilename: '[name].js'
    },
    optimization: {
        splitChunks: {
            name: 'vendor',
            chunks: 'initial'
        }
    },

これで、node_modules に含まれている共有ライブラリが vendor.js にまとめられます。

App.html への動的ローディングの追加

App.html にリンクハッシュに基づいて src/views/ 以下の html ファイルを読み込むよう記述を加えます。

App.html
{#if view}
  {#await view}
    Now loading...
  {:then module}
    <svelte:component this={module.default} />
  {:catch error}
    Fails to load: {error.message}
  {/await}
{/if}

<script>
import { createHashHistory } from 'history';

const history = createHashHistory();

export default {
    oncreate() {
        this.unlisten = history.listen(location => {
            let path = location.pathname;
            if (!path.startsWith('/') {
                path = '/' + path;
            }
            if (path.endsWith('/')) {
                path += 'Index';
            }
            this.set({
                view: import(
                  /* webpackChunkName: 'views/[request]' */
                  `/views${path}.html`
                );
            });
        });
    },
    ondestroy() {
        this.unlisten();
    },
    data() {
        return {
            view: null
        };
    }
};
</script>

以下では、処理を細かく見ていきます。

history による URL ハッシュからのパス取得

次のコードでは history を使って URL ハッシュの変更イベントからアクセスするパスを拾っています。

hisotry は React Router などでも使われている URL 変更イベントや履歴を統一的に扱うことができるラッパークラスです。ここでは、createHashHistory を使っていますが、createBrowserHistory を使えば URL のパスで遷移することもできます。1URLを打ち込むとサーバー側に処理が渡ってしまい面倒なので、ここではハッシュを使っています。

oncreate() {
    this.unlisten = history.listen(location => {
        ...
    }
},
ondestroy() {
    this.unlisten();
},

import 関数によるモジュールの動的ロード

import 関数は、現在 TC39 で検討が進められている動的インポートの仕組みです。webpack を使うと先行して使うことができます。

ただし、使い方に多少工夫が必要です。import 関数の引数が固定にならないため、webpack はどのファイルをバンドルすればよいのかわかりません。そこで、import 関数の引数にマジックコメントでヒントを追加する必要があります。

view: import(
   /* webpackChunkName: 'views/[request]' */
   `/views${path}.html`
);

ここでは、webpackChunkName を指定していますが、views フォルダ以下のリクエストされたファイルをロードするよう指定しています。これにより views 以下のファイルをチャンクに分割し、ワイルドカードによる動的ローディングが実現できます。

import 関数の戻り値は、モジュールになります。読み込み先のモジュールを export default で公開している場合には、モジュールの中身は戻り値の default フィールドに格納されていることに注意してください。また、import 関数に指定できるヒントについては webpack のマニュアルを参照してください。

なお、import 関数は、まだ仕様確定に至っていないので、Babel を通す場合には、babel-plugin-syntax-dynamic-import が必要です。

await ブロックによるモジュール・ロードの監視

await ブロックを使うと Promise の状態を簡単に監視することができます。

ここでは、data として Promise を設定する view を定義しています。ロードが終わった段階で取得したモジュールを取り出し、svelte:component に設定します。

{#if view}
  {#await view}
    Now loading...
  {:then module}
    <svelte:component this={module.default} />
  {:catch error}
    Fails to load: {error.message}
  {/await}
{/if}

<script>
...
data() {
    return {
       view: null
    }
}
</script>

なお、await ブロックは、評価式が Promise でない(例えば undefined)場合 then 句に進むため、if ブロックで囲むほうがよいようです。


  1. BrowserHistory は、通常のサイトと同じように URL が表示されるため一見良さそうなのですが、URLを打ち込むとサーバー側にリクエストが行ってしまい 404 Page Not Found になってしまうのが難点です。