上間ウェブ店

【脱gulp】webpackでpugを使う。jsonで一括ビルドも。

gulpでのコーディング環境の記事はけっこうな量を見つける事ができますが、webpackになるととたんに少なくなるのでニッチな所を狙ってPV稼ぎしたいと思います。

gulpも悪くないが、jsのコンパイルでwebpackを使うなら、最初からwebpackベースの方がいい

タイトルのpugに限らず、gulpでコーディング環境を組んでいる人は多いと思います。

ejsやpugのテンプレートエンジン、sassやstylusのcssメタ言語は問題ないとして、jsを1ファイルで出力しようとするとどうしてもwebpackが必要になると思います。(他にもありますが、現在のデファクトスタンダードとして)

実際それでも問題ないと思いますが、ツールをあれこれ使うより1つにまとめたくなるのがコーダーって人種でしょう。gulpでできるほとんどの事はwebpackでできるので、webpackに乗り換えたくなるものです。

という訳で、今回はwebpackでpugを使う方法を紹介します。

ちなみに、webpack自体のインストールや使い方などは省略します。

参考サイト

前提

  • 作業フォルダは”src”、出力フォルダは”dist”
  • pugのファイルは”src/documents”をルートにし、階層を保って置く

ローダーは”pug-loader”

webpackでjs以外の言語を使おうとすると、loaderというものが必要となります。

loaderも1言語に1つだけではなく、開発者が自分が使いやすいloaderを開発していたりして、どれを使えばいいかわからなくなる事もあります。

pugの場合だと、”pug-loader”, “pug-html-loader”などがあり、データの渡す方法で使い方が違ったりと、僕もいろいろ悩みました。

結果、”pug-loader”の方が使いやすいという事でこっちを採用しました。

使い方はこんな感じです。オプションのルートを書くとpugのインクルード使う時に楽になります。

// webpack.config.js

module.exports = {

  ... 省略

  module: {
    rules: [
      {
        test: /\.pug$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'pug-loader',
            options: {
              pretty: true,
              root: path.resolve(__dirname, 'src/documents'),
            }
          },
        ],
      },
    ]
  }
}

HtmlWebpackPlugin を使う

webpackでpugを使用してhtmlファイルを出力するには、HtmlWebpackPluginを利用するのが簡単です。

参考サイトのSPA ではない Webpack 設定サンプルが分かりやすいです。

webpack.config.jsのpluginsの所にこんな感じで書いてあげれば出力してくれます。(前項のloaderに追加します)

// webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {

  ... 省略

  module: {
    rules: [
      {
        test: /\.pug$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'pug-loader',
            options: {
              pretty: true,
              root: path.resolve(__dirname, 'src/documents'),
            }
          },
        ],
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.pug',
    }),
  ]
}

とても単純でわかりやすいんですが、ファイルを増やす度にpluginのとこに追記していくなんて事はしたくないですよね。こんな感じのやつ。

// webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {

  ... 省略

  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.pug',
    }),
  new HtmlWebpackPlugin({
      filename: '/test/index.html',
      template: '/test/index.pug',
    }),
  new HtmlWebpackPlugin({
      filename: '/test/index2.html',
      template: '/test/index2.pug',
    }),
    ... ここに永遠に手動で書くなんてやってられない
  ]
}

そこで登場するのが “globule” です。

globule

globuleは大雑把な説明ですが、ファイルを全部探してきてくれるやつです。

ここでは、”.pug”と付くファイルを作業フォルダから探してきて配列にするという使い方をしています。

その時に、pugでhtmlファイルとしては出力したくない物もあると思います。

includeされるようなヘッダーやフッターなどのモジュールたちや、テンプレートファイルたちです。

こいつらは、”_modules”や”_layout”というようにアンダーバーをprefixとしたフォルダ名のとこに入れておきます。

そうすると、globuleで除外しやすくなります。ここまでの流れのコードを全部まとめてみます。

// webpack.config.js

const globule = require('globule')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {

  ... 省略

  module: {
    rules: [
      {
        test: /\.pug$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'pug-loader',
            options: {
              pretty: true,
              root: path.resolve(__dirname, 'src/documents'),
            }
          },
        ],
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: `index.html`,
      template: 'index.pug',
    }),
  ]
}

const documents = globule.find(
  './src/documents/**/*.pug', {
    ignore: [
      './src/documents/**/_*/*.pug'
    ]
  }
)

これまでやると、後はHtmlWebpackPluginで手動で追加していた所を、ループで回して自動で追加してやるだけです。

forでまわしてpugファイル分のHtmlWebpackPluginを発火させる

さっきの項目で、出力したいpugファイルを全部取得しました。

こいつをループして追加するには少し変更を加えます。

↓これを

// before

module.exports = {

  ... 省略

}

↓こうします

// after

const app = {

  ... 省略

}
module.exports = app

objectをそのまま出力するか、変数にして変数を出力するかの違いです。

組み合わせるとこうなります。

// webpack.config.js

const globule = require('globule')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const app = {

  ... 省略

  module: {
    rules: [
      {
        test: /\.pug$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'pug-loader',
            options: {
              pretty: true,
              root: path.resolve(__dirname, 'src/documents'),
            }
          },
        ],
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: `index.html`,
      template: 'index.pug',
    }),
  ]
}

const documents = globule.find(
  './src/documents/**/*.pug', {
    ignore: [
      './src/documents/**/_*/*.pug'
    ]
  }
)

module.exports = app

ここに、globuleで取得したpugのファイルたちをループする記述を加えます。(ループで出力するので最初に書いたpluginのとこは削除します)

nodeもjsなので記述は普通のjavascriptですね。

// webpack.config.js

const globule = require('globule')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const app = {

  ... 省略

  module: {
    rules: [
      {
        test: /\.pug$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'pug-loader',
            options: {
              pretty: true,
              root: path.resolve(__dirname, 'src/documents'),
            }
          },
        ],
      },
    ]
  }
}

const documents = globule.find(
  './src/documents/**/*.pug', {
    ignore: [
      './src/documents/**/_*/*.pug'
    ]
  }
)

documents.forEach((document) => {
  const fileName = document.replace('./src/documents/', '').replace('.pug', '.html')
  app.plugins.push(
    new HtmlWebpackPlugin({
      filename: `${fileName}`,
      template: document,
    })
  )
}

module.exports = app

追加したループのところで何をしてるかというと、こんな感じです。

  • 取得した内容は、パスが入ってるので、パスから要らない記述を削除とpugをhtmlに変換
  • そうすると、残るのはフォルダ名+ファイル名(フォルダ名がある場合は残るし、無い場合はファイル名のみ)
  • filenameは出力したいファイル名(ここではhtml)
  • templateは取得したファイルそのまま
  • templateをfilenameにして出力するってイメージ

以上がpugをwebpackで使えるようにする方法です。次はこれにjsonによる一括生成も追加します。

jsonで一括生成

一括生成する方法は、jsonでどういった内容を管理するか、階層などをどうするかといった設計が必要なんですが、ここでは出力後のサイト構造は下記のようにしたいと思います。各ディレクトリのindex.htmlは量産対象外で、ディレクトリの中に量産ファイルを出力するという形です。

top
  ┣ sample
     ┣ index.html   // 量産対象外
     ┣ sample1.html // 量産対象
     ┣ sample2.html // 量産対象
     ┣ sample3.html // 量産対象
     ┣ sample4.html // 量産対象
     ┣ ... ここに増えていく

作業フォルダの構造とjsonの中身

ファイル名は何でもいいのですが、量産するjsonとtemplateは同階層に置きます。

src/
 ┣ documents/
    ┣ index.pug
      sample/
       ┣ index.pug
       ┣ _template.pug
       ┣ _data.json
// data.json

{
  "sample1": {
    "title": "sample1のページ",
    "description": "sample1を紹介するページです。sample1のあんな事やこんな事が書いてます。",
    "eyecatch": "/images/sample1.jpg",
    "feature": "sample1は○○が特徴的なツールです。○○な時に活躍するでしょう。",
    "usage": [
      "○○をダウンロード",
      "○○をインストール",
      "コマンドを打つ"
    ],
    "price": "¥1,000(税込)"
  },
  "sample2": {
    "title": "sample3のページ",
    "description": "sample2を紹介するページです。sample2のあんな事やこんな事が書いてます。",
    "eyecatch": "/images/sample3.jpg",
    "feature": "sample2は○○が特徴的なツールです。○○な時に活躍するでしょう。",
    "usage": [
      "○○をダウンロード",
      "○○をインストール",
      "コマンドを打つ"
    ],
    "price": "¥2,000(税込)"
  },
  "sample3": {
    "title": "sample3のページ",
    "description": "sample3を紹介するページです。sample3のあんな事やこんな事が書いてます。",
    "eyecatch": "/images/sample3.jpg",
    "feature": "sample3は○○が特徴的なツールです。○○な時に活躍するでしょう。",
    "usage": [
      "○○をダウンロード",
      "○○をインストール",
      "コマンドを打つ"
    ],
    "price": "¥3,000(税込)"
  }
}

webpack.config.jsの書き方

先ほどのforで回して追加する所に条件分岐を加えて、dataを渡せるように工夫します。

// before

documents.forEach((document) => {
  const fileName = document.replace('./src/documents/', '').replace('.pug', '.html')
  app.plugins.push(
    new HtmlWebpackPlugin({
      filename: `${fileName}`,
      template: document,
    })
  )
}
// after

documents.forEach((document) => {
  if (document.match(/_template/)) {
    const dirName = document.replace('./src/documents/', '').replace('/_template.pug', '')
    const replaceJson = document.replace('_template', '_data').replace('.pug', '.json')
    const json = require(replaceJson)
    Object.keys(json).forEach((f) => {
      const fileName = f
      app.plugins.push(
        new HtmlWebpackPlugin({
          filename: `${dirName}/${fileName}.html`,
          template: document,
          data: json[f],
        })
      )
    })
  }
  else {
    const fileName = document.replace('./src/documents/', '').replace('.pug', '.html')
    app.plugins.push(
      new HtmlWebpackPlugin({
        filename: `${fileName}`,
        template: document,
      })
    )
  }
}

さっきのjsonを使わないpugはelseのところで、追加した方が上側ですね。

何をしてるかというと、こんな感じです。

  • _templateというpugファイルだった場合
  • dirName -> 対象のディレクトリを特定
  • replaceJson -> 対象のjsonファイル名を特定
  • json ->特定したファイルをjsonとして読み込む
  • Object.keys で、jsonのkeyの分だけループさせる
  • データを渡す

渡すデータですが、上記だと、data[f]のところは1ページ分の情報だけ入るようになっています。やり方次第でjsonのデータ全てを渡す事もできます。

データの受け取り方

渡したデータは、pugのtemplateファイルで受け取ります。こんな感じで使う事ができます。

- var data = htmlWebpackPlugin.options.data
- var pageInfo = {
  path: htmlWebpackPlugin.options.data.key,
  title: htmlWebpackPlugin.options.data.title,
  description: htmlWebpackPlugin.options.data.description
}

h1 #{pageInfo.title}
p.lead #{pageInfo.description}

おわりに

以上がwebpackでpugを使う方法とjsonによる一括生成の方法でした。

webpack.config.jsの書き方はもう少しスマートな方法もあるかもしれませんが、とりあえず全項目をまとめるとこちらになります。

// webpack.config.js

const globule = require('globule')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const app = {

  ... 省略

  module: {
    rules: [
      {
        test: /\.pug$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'pug-loader',
            options: {
              pretty: true,
              root: path.resolve(__dirname, 'src/documents'),
            }
          },
        ],
      },
    ]
  }
}

const documents = globule.find(
  './src/documents/**/*.pug', {
    ignore: [
      './src/documents/**/_*/*.pug'
    ]
  }
)

documents.forEach((document) => {
  if (document.match(/_template/)) {
    const dirName = document.replace('./src/documents/', '').replace('/_template.pug', '')
    const replaceJson = document.replace('_template', '_data').replace('.pug', '.json')
    const json = require(replaceJson)
    Object.keys(json).forEach((f) => {
      const fileName = f
      app.plugins.push(
        new HtmlWebpackPlugin({
          filename: `${dirName}/${fileName}.html`,
          template: document,
          data: json[f],
        })
      )
    })
  }
  else {
    const fileName = document.replace('./src/documents/', '').replace('.pug', '.html')
    app.plugins.push(
      new HtmlWebpackPlugin({
        filename: `${fileName}`,
        template: document,
      })
    )
  }
}

module.exports = app

webpackでテンプレートエンジンの記事は少ないので、誰かの参考になれば嬉しいです。