webpackだけでpugをビルドする環境を作る


始めに

今までgulpでコーディングの環境を作って、JSだけwebpackするというものを使っていましたが、それを何とかwebpack側に全部移行できないかなと思っていました。ただpugをビルドする方法がなかなかいいのが見つからなくて結構苦労しましたが、ある程度形になったのでそれを記事にまとめたいと思います。

動作確認用のリポジトリはこちらになっているので参考にしてください。色々余計なコードは入っていますが・・・。

コーディング環境リポジトリ


pugのビルド

ReactやVue.jsの開発でお世話になっているHTMLWebpackPluginは動的のファイル追加に対応できないので今回は使用しません。

今回はpug-html-loaderというものを使います。以下のような感じでローダーを呼んでいくとpugでビルドしてHTMLを出力してくれます。


pugのルール設定

module.exports = {

// 他の設定は省略
module: {
rules: [
{
test: /\.pug$/,
use: [
{
loader: 'file-loader',
options: { name: '[name].html' }
},
'extract-loader',
{
loader: 'html-loader',
options: {
attrs: ['img:src', ':data-src']
}
},
{
loader: 'pug-html-loader',
options: {
pretty: true
}
}
]
},
]
}
};

このようにするとCSSと同じようにJS側でimport文を書くとHTMLを出力してくれます。


CSSと同じ感じでimportするやり方

import './pug/index.pug';   // index.htmlが生成される


ただ今回は動的にimport出来るようにするため、require.contextを使用します。


require.contextでディレクトリ以下をまとめてimportするやり方

// pugディレクトリ以下のファイルを全部取得する

const req = require.context('../pug/', false, /\.pug/);
// ファイル一覧を取得して、それぞれrequireする
req.keys().forEach((fileName) => {
req(fileName);
});


pugのビルド後のリロード

これでめでたしめでたしといきたいところですが、なぜかリロードしないという問題にぶち当たってしまいました・・・。色々調べたところ、どうやらwebpack-dev-serverでhot reloadの設定をするとリロードしなくなるようです。ただこれを外すとCSSがホットリロードしなくなってしまうため、やりたくない。

そんなわけでbrowser-syncを使ってpugファイルが更新したときに強制リロードするという強行に出ます。


browser-syncサーバーを使用してwebpackを使う

const browserSync = require('browser-sync');

const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');

const webpackConfig = require('../webpack.config.development');
const bundler = webpack(webpackConfig);

const webpackDevMiddlewareInstance = webpackDevMiddleware(bundler, {
publicPath: webpackConfig.output.publicPath,
stats: {
colors: true
}
});

const server = browserSync({
port: 3000,
ghostMode: false,
server: {
baseDir: 'dist',
middleware: [
webpackDevMiddlewareInstance,
webpackHotMiddleware(bundler)
]
},
files: [
{
// pugファイルを更新してもなぜかリロードしてくれないので手動で更新する
match: ['./src/pug/**/*.pug'],
fn: (event, file) => {
// 一応ビルドを待つイベントっぽいけど、確信はない
webpackDevMiddlewareInstance.waitUntilValid(() => {
console.log('finish');
server.reload();
});
}
}
]
});


これでなんとかpugをビルドしたらオートリロードしてくれました。mixinとかの変更にもちゃんとビルドしてくれるか心配でしたがサンプルの段階では一応大丈夫そうです。


CSSのホットリロード

CSSのホットリロードは以下のような感じに書いたら動きました。最近のwebpack-dev-serverはentryにホットリロード用のモジュールを入れる必要はなかったのですが、browser-sync経由でビルドする場合は必要なようです。


webpack.config.development.js

// entryにhot-middlewareを追加する

for (const key in config.entry) {
if (config.entry.hasOwnProperty(key)) {
config.entry[key].unshift('webpack/hot/dev-server'); // 多分こっちはいらない気がする
config.entry[key].unshift('webpack-hot-middleware/client');
}
}

config.module.rules.push({
test: /\.(sass|scss)$/,
use: [
'css-hot-loader',
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: true,
// 0 => no loaders (default);
// 1 => postcss-loader;
// 2 => postcss-loader, sass-loader
importLoaders: 2
}
},
{
loader: 'postcss-loader',
options: {
sourceMap: true,
plugins: [
autoprefixer({
browsers: [
'last 2 version',
'IE 11'
]
})
]
}
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
includePaths: [path.resolve('./node_modules/')]
}
},
]
});



JSのリロード

JSはいつも通りbabel-loaderの設定だけでいいかなと思っていたのですが、browser-syncからのビルドによってなぜかホットリロードに失敗してもリロードしてくれなかったので強制的にリロードするコードを書きました。


entry.js

// HMRに失敗してもJSがリロードしてくれないので強制的にリロードを実行する

if (module.hot) {
module.hot.accept(console.error);
module.hot.dispose(() => {
window.location.reload();
});
}


Reactのホットリロード

余談ですが、今の環境ならReactもホットリロードできるんじゃないかと思って試してみました。

ホットリロードの更新は子から親へ伝搬し、ホットリロードが出来る場所で伝搬が止まるので、以下のようにしたら上手くReactだけホットリロードしてくれました。Vue.jsはどこでホットリロードの設定を書いているのかわかりづらいのですが、おそらく同じようにできるのではないでしょうか。

Reactの動作はブランチを切っているので、興味がある方はこちらのブランチの方を参考にしてみてください。

コーディング環境+Reactのブランチ


entry.js

import './pugImport';

import '../css/base.scss';

// reactのエントリーポイント
import './react/reactEntry';

console.log('test');

// HMRに失敗してもJSがリロードしてくれないので強制的にリロードを実行する
if (module.hot) {
module.hot.accept(console.error);
module.hot.dispose(() => {
window.location.reload();
});
}



reactEntry.js

import React from 'react';

import { render } from 'react-dom';

import App from './App';

render(<App/>, document.getElementById('app'));



App.jsx

import React from 'react';

import { hot } from 'react-hot-loader/root';

import MyComponent from './components/MyComponent';

class App extends React.Component {
render() {
return (
<div>
<div>Hello, World!</div>
<MyComponent/>
</div>
);
}
}

export default hot(App);



おわりに

pugをwebpackでビルドしたいだけなのになかなかいい方法が見つからずかなり苦戦しました。しかし全部webpackでビルドできたことでfile-loaderやurl-loaderがHTMLやCSSにも適応できて、パスの解決やblob化などがしやすくなりました。最初は考えていなかったのですが、基本はjQueryだけど一部はReactで書くと言った場合に今まではフルリロードも止む無しと思っていたのがここだけホットリロードを効かせられたり、結構メリットは大きいんじゃないかなと思いました。

ちなみに今回は諦めていますが、pug側でもしrequireが使えるようになったらCSS Moduleが使えるようになり、パーツをmixinで作るとコーディングでもクラス名の汚染をかなり防げるんじゃないかなという夢も感じました。