Webpackを頑張って設定して、すごい静的サイトジェネレータとして使おう

目標

この記事のゴールは、Webpackで以下のことができる設定を紹介することです。

  • SPAではなく複数のHTMLをPugを利用して生成する
  • 複数のJSをそれぞれBabelでトランスパイルする(1つにはまとめない)
  • 複数のSassもそれぞれコンパイルする(1つにはまとめない)
  • CSSにAutoprefixerなどのpostcssをかける
  • CSSを部分的にインラインにもしたい
  • トランスパイルしないファイル(.pngみたいなもの)も扱いたい

先に結果から

とりあえず webpack.config.jsを貼っておきます。

webpack.config.js
// yarn add apply-loader autoprefixer babel-core babel-loader babel-preset-env copy-webpack-plugin css-loader extract-text-webpack-plugin globule node-sass postcss postcss-loader pug pug-loader sass-loader style-loader webpack webpack-dev-server

// develop : webpack-dev-server --open
// build   : NODE_ENV=production webpack

const webpack = require('webpack')
const path = require('path')
const globule = require('globule')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')

// ディレクトリの設定
const opts = {
  srcDir: path.join(__dirname, 'src'),
  destDir: path.join(__dirname, 'public')
}

// keyの拡張子のファイルが、valueの拡張子のファイルに変換される
const convertExtensions = {
  pug: 'html',
  sass: 'css',
  js: 'js'
}

// トランスパイルするファイルを列挙する
// _から始まるファイルは、他からimportされるためのファイルとして扱い、個別のファイルには出力しない
const files = {}
Object.keys(convertExtensions).forEach(from => {
  const to = convertExtensions[from]
  globule.find([`**/*.${from}`, `!**/_*.${from}`], {cwd: opts.srcDir}).forEach(filename => {
    files[filename.replace(new RegExp(`.${from}$`, 'i'), `.${to}`)] = path.join(opts.srcDir, filename)
  })
})

// pugでトランスパイルする
const pugLoader = [
  'apply-loader',
  'pug-loader'
]

// Sassをトランスパイルし、autoprefixerをかけるようにする
const sassLoader = [
  {
    loader: 'css-loader',
    options: {
      minimize: true
    }
  },
  {
    loader: 'postcss-loader',
    options: {
      ident: 'postcss',
      plugins: (loader) => [require('autoprefixer')()]
    }
  },
  'sass-loader'
]

// Babelでトランスパイルする
const jsLoader = {
  loader: 'babel-loader',
  query: {
    presets: ['env']
  }
}

const config = {
  context: opts.srcDir,
  entry: files,
  output: {
    filename: '[name]',
    path: opts.destDir
  },
  module: {
    rules: [
      {
        test: /.pug$/,
        use: ExtractTextPlugin.extract(pugLoader)
      },
      {
        test: /.sass$/,
        oneOf: [
          {
            // pugから `require('./hoge.sass?inline')` のように呼ばれた時は、ExtractTextPluginをかけない
            resourceQuery: /inline/,
            use: sassLoader
          },
          {
            // それ以外の時は、単純にファイルを生成する
            use: ExtractTextPlugin.extract(sassLoader)
          }
        ]
      },
      {
        test: /.js$/,
        exclude: /node_modules(?!/webpack-dev-server)/,
        use: jsLoader
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin('[name]'),
    // convertExtensionsに含まれていないファイルは、単純にコピーする
    new CopyWebpackPlugin(
      [{from: {glob: '**/*', dot: true}}],
      {ignore: Object.keys(convertExtensions).map((ext) => `*.${ext}`)}
    ),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
    })
  ],
  devServer: {
    contentBase: opts.destDir,
    watchContentBase: true
  }
}

if (process.env.NODE_ENV === 'production') {
  config.plugins = config.plugins.concat([
    new webpack.optimize.UglifyJsPlugin(),
    new webpack.optimize.OccurrenceOrderPlugin(),
    new webpack.optimize.AggressiveMergingPlugin()
  ])
}

module.exports = config

以下は、この設定ファイルの説明だけなので、困った時にでも読んでください。

動機

僕はRailsが大好きですが、動的に生成する必要が無いサイトを作ることもたくさんあります。
そんな時に、今の時代HTML/CSS/JSを生で書くわけにもいかないので、トランスパイルしたくなるけど、どの静的サイトジェネレータもいまいちしっくりこない。
できれば、CSSはSassを使ってトランスパイルしたいし、autoprefixしてほしい。JSもバベりたい。

ということでWebpackをコテコテに設定して、いい感じにトランスパイルしてくれるようにしました。

仕組み

「トランスパイルするもの」と「それ以外」

トランスパイルするものは、Webpackの設定で、
それ以外のものは、そのままCopyWebpackPluginというもので、opts.destDirにコピーします。

複数ファイル出力

Webpackはentryに複数のキーを持ったObjectを指定すると、

{
  "出力先ファイル名": "ソースファイル"
}

の形で複数ファイルを出力してくれます。

これを利用して、JSもHTMLもCSSも、一緒くたに生成してしまいましょう。

HTML

まずHTMLですが、僕はPugが好きなので、Pugを使います。

pug-loaderはpugテンプレートから、requireすると「HTMLを生成するようなfunction」を返してくれるJSを生成するLoaderなので、これだけだと困ります。
なのでapply-loaderでfunctionを一度呼んでHTMLを生成することにします。

テンプレートのファイルは_hoge.pugのようにアンダーバーから始まるファイル名にすれば、トランスパイルのリストから除外することができます。

アクセスするURLをきれいにしたりしたい時は、filesのキーをいい感じに、いじると良さそうです。

SASS

<link>タグで読み込むCSSはそのまま、Sassで書きましょう。
link(rel="stylesheet" href=`/hoge.css?${+ new Date()}`)
みたいに書くと、キャッシュを無効化できて便利です。
(本当はWebpackのHashを使いたかったのですが、Pugから取得することが難しかったので今回はやめました)

<style>タグで、インラインに展開したいCSSは、
style= require("./hoge.sass?inline")
の様に書けば、インラインで展開できます。

インライン展開したい時は、ExtractTextPluginをかけないようにしないと、変な展開のされ方をしてしまいます。

@importで読み込むSassは、名前をアンダーバーから始めれば、それ単体ではトランスパイルされなくなります。

JS

JSは普通にBabelでトランスパイルするだけです。
Webpackの本来のお仕事なので、特に難しいことはありません。

その他

webpack-dev-serverですがJSを読み込んでいないHTMLでは、自動リロードが効かないようです。
これを直す方法が未だに見つけられていないので、もし知っている方がいたら教えてください。

それから今の所、ファイルが新規追加されたときには、手動でwebpack-dev-serverを起動し直さなければなりません。何かしら便利なコマンドなどを知っている方がいらっしゃいましたら、こちらも教えていただけると助かります。

関連記事