FULL STUCK DIARY

だいたい行き詰まっている備忘録

Webpack(Vue-CLI3)で静的pugファイルのhtml生成と格闘した

環境構築の大敵便利ツール、Webpack。自分は本格的に使い始めて約半年、設定ファイルがいまだにほとんど理解できてないので固有のことをしようとすると毎回手こずります。さらにVue CLI3ではWebpackは包含されてしまっているのでよりややこしい部分があります。今回も例に漏れず苦難の連続だったのでメモ。

環境設定はこれの続きです。

ゴール

一言でいうとテンプレート的に作ったpugファイルにjsonからいくつかのデータを渡して、複数のディレクトリに内容違いのhtmlファイルを生成したい。整理すると以下です。

入力

  • index.pug (本文は変数になっている)
  • data.json (複数のオブジェクトの配列になっている)

出力

  • a/index.html
  • b/index.html
  • c/index.html
  • ...(全てレイアウト及び機能は同じだが文言が違う)

どうあがいてもわかりづらい。難所はいくつかあるのですが1つずつ解決していきます。

複数のentryから複数のディレクトリにoutputを出力

これは実は非常に簡単です。pagesに設定する時点で出力後の生成ファイルに期待する相対パスhushとして指定すればそのままのパスで出力されます。ファイルが多い時は処理用のパッケージがあるらしい。

qiita.com

極めて言葉だけでは伝わりづらいところ特に図解もしないのはこちらにアクセスすればすべてわかるからです。では次。

1つのentryから複数のhtmlを出力

上の項の応用ですね。これもまあ簡単です。シンプルに同じ entry および template を持っているが output が違うパラメータオブジェクトを量産してpagesに渡すだけです。

const meta = require('data.json');
(() => {
    meta.forEach(d => {
        index[`page-${d.id}`] = {
            entry: ENTRY,
            template: MAIN_TEMPLATE,
            filename: `${d.id}/index.html`,
        };
    });
})();

こんな感じ。index['page-${d.id}'] この部分のキー page-${d.id} はのちにパラメータを追加するときの識別子になります。

pugにオブジェクトを渡す

最大と言っていい難所。jsonからpugにデータを渡していきます。

ちなみにここで知りましたが、ターミナルで

$ vue inspect > result.js

とやると最終的にどんなプラグインがどのパラメータをもってどのように適用されているのか見ることができます。vue.config.js では省略されている new HtmlWebpackPlugin() とか見られます。

qiita.com

全ページ共通のデータを渡す

pug-plain-loaderdata オプションを使います。

まずはvue add pug でお手軽にセットアップしたので、実際何が入ってるのか調査します。vue-cli-plugin-pugのソースを見ると

    webpackConfig.module
      .rule('pug')
        .test(/\.pug$/)

        // this applies to <template lang="pug"> in Vue components
        .oneOf('vue-loader')
          .resourceQuery(/^\?vue/)
          .use('pug-plain')
            .loader('pug-plain-loader')
            .end()
        .end()

        // this applies to pug imports inside JavaScript, i.e. .pug files
        .oneOf('raw-pug-files')
          .use('pug-raw')
            .loader('raw-loader')
            .end()
          .use('pug-plain')
            .loader('pug-plain-loader')
            .end()
        .end()

こんな感じでチェインされているのがわかりました。これを使って自分の vue.config.js の chainwebpack に

    chainWebpack: config => {
        config.module.rule('pug').uses.clear();
        config.module
            .rule('pug')
            .oneOf('raw-pug-files')
            .use('pug-plain')
            .loader('pug-plain-loader')
            .options({
                root: path.resolve('src/pug/'),
                data: object
            })
            .end()
    }

上書きしつつオプションを渡します*1vue inspectすると

          /* config.module.rule('pug').oneOf('raw-pug-files') */
          {
            use: [
              {
                loader: 'raw-loader'
              },
              {
                loader: 'pug-plain-loader',
                options: {
                  root: '/path/to/src/pug',
                  data: { <object> }
                }
              }
            ]
          }

ちゃんとoptionsが無駄なく*2渡っているのがわかります。servebuildも成功しデータが渡っているのを確認できました。

個別のデータを渡す

html-webpack-plugin の templateParameters を使います。

    chainWebpack: config => {
        config.module.rule('pug').uses.clear();
        meta.forEach((data, num) => {
            config
                .plugin(`html-page-${data.id}`)
                .tap(args => {
                    args[0].templateParameters = {
                        ...data,
                        num
                    };
                    return args;
                });
        });
    },

ここで plugin に渡している html-page-${data.id} はhtml出力用の pages に渡したオブジェクト群のkeyに対応しています。vue inspect するとこう

    new HtmlWebpackPlugin(
      {
        templateParameters: {
          <dataの中身>,
          num: 0
        },
        chunks: [
          'chunk-vendors',
          'chunk-common',
          'page-<id>'
        ],
        template: '/path/to/src/pug/index.pug',
        filename: '<id>/index.html',
        title: undefined
      }
    ),
   new HtmlWebpackPlugin(
      ...

pagesに渡したオブジェクトの数だけ、それぞれのデータを含んだ状態で、 HtmlWebpackPlugin インスタンスが生成されているようです。ここからが問題。

次に vue add pug でインストールした vue-cli-plugin-pug をアンインストールして pug と pug-loader と pug-plain-loader と raw-loader をインストールします。(?!)

前項の続きでいけるかと思ったんですが、パラメータの読み込み順をいじれない問題で断念しました。どういうことかというと以下。

configureWebpackをこんな感じで設定します。

    configureWebpack: {
        module: {
            rules: [{
                test: /\.pug$/,
                oneOf: [
                    // this applies to `<template lang="pug">` in Vue components
                    {
                        resourceQuery: /^\?vue/,
                        use: ['pug-plain-loader'],
                    },

                    {
                        test: /index/,
                        use: ['pug-loader'],
                    },

                    // this applies to pug imports inside JavaScript
                    {
                        use: [
                            'raw-loader',
                            {
                            loader: 'pug-plain-loader'
                            },
                        ],
                    },
                ],
            }]
      ....
        }
     }

test: /index/ のindexはテンプレートとして使いたpugファイル名を入れます。

OneOf の中に3つのオブジェクトがあるわけですが、これは vue-cli-plugin-pug の index.js にある2つのルールの間に、まんまもう1つ割り込ませています。これはこの位置(raw-loader と pug-plain-loaderの前)にないとtemplateParameterが適用されないんですよね……chainWebpackで上書きする方法では最後尾にルールを付け足すことしかできないので、パラメータが読み込まれませんでした。ちなみに vue-cli-plugin-pug の index.js に直接ルールを付け足しても動くには動きます。

    webpackConfig.module
      .rule('pug')
        .test(/\.pug$/)

        // this applies to <template lang="pug"> in Vue components
        .oneOf('vue-loader')
          .resourceQuery(/^\?vue/)
          .use('pug-plain')
            .loader('pug-plain-loader')
            .end()
        .end()

       // 真ん中に割り込み
        .oneOf('pug-template')
          .test(/index/)
          .use('pug')
            .loader('pug-loader')
            .end()
        .end()

        // this applies to pug imports inside JavaScript, i.e. .pug files
        .oneOf('raw-pug-files')
          .use('pug-raw')
            .loader('raw-loader')
            .end()
          .use('pug-plain')
            .loader('pug-plain-loader')
            .end()
        .end()

node_modules の中をいじるとあとあとどういう地獄を見るのかゾクゾクしますね。

まとめ

というわけで色々回り道をしながらpugから内容違いのhtmlを生成することに成功しました。そもそもの話ですけどこれあまりVue-CLIで想定されている使い方ではない気がする。そういう意味で生産性が若干疑問ですが、各種レポジトリのissueやコードをの情報を駆使して問題解決する経験が図らずもできたのでよしとします。

*1:絶対もっとスマートな方法がある気がする

*2:ちなみに上のルールでruleとかoneOfとかを省略すると正しく認識されずここがややこしいことになります。