小規模なサイトでもwebpackを使って効率的にビルドしてサイト構築する方法です。
一般的なサイト構成のテンプレートとして使っていただけると思います。
条件
保守性の観点から、以下の点を満たすことを条件とします。
- webpackだけで完結したい
- 開発用ディレクトリと公開ディレクトリを分離したい
- 公開ディレクトリにはwebpackからすべてのリソースを書き出したい(html,js,css,image)
- HTML内の共通コンポーネントは外部ファイル化して読み込ませる形にしたい
- 共通コンポーネント内で変数を使いたい
- CSSはSASS(SCSS)を使いたい
- JSはES6(ES2015)で書いてコンパイルしたい
- 修正時の保守性を高くしたい(毎回configを書き換えるような形にしたくない)
作りたいページの構成
ページ構成は以下のような、よくある感じの構成とします。
リソースはすべてassetディレクトリ配下に格納します。
お問い合わせ(contact)ページはphpでスクリプトを動かし、入浴画面(input)と完了画面(complete)はphpにてhtmlをincludeしてレンダリングする形とします。
/
├ assets/ - リソース一式
│ └ img/ - 画像
│ └ css/ - CSS
│ └ js/ - JavaScript
│
├ index.html - TOPページ
├ products.html - 製品ページ
├ company.html - 会社概要
├ contact.php - お問い合わせ
├ contact-input.html - お問い合わせ入力画面
├ contact-complete.html - お問い合わせ完了画面
└ privacy.html - プライバシーポリシー
なおHTMLはdev直下に通常通りコーディングしたものを保存します。(ejsによるテンプレート化は後述)
ディレクトリ構成
/
├ dev/ (開発用ディレクトリ)
│ └ assets/
│ └ img/
│ └ css/ (※scssファイルを保存)
│ └ js/
├ dist/ (公開ディレクトリ)
│ └ assets/
│ └ img/
│ └ css/
│ └ js/
├ node_modules(nodejsにより自動作成)
├ package.json
├ postcss.config.js
└ webpack.config.js
##postcss.config.js
本当はwebpackのconfigにまとめたいのですが、なぜかpostcss.config.jsがないよというエラーになり実行できないので別ファイルにしました。
module.exports = {
plugins: [
require('autoprefixer')
]
}
webpack.config.jsの書き方
###基本のentryとoutput
npm install --save-dev webpack
"use strict";
const path = require("path");
const webpack = require("webpack");
module.exports = {
devtool: 'inline-source-map',
entry: {
index : path.join(__dirname, "dev/assets/js/index.js"),
products : path.join(__dirname, "dev/assets/js/products.js"),
company : path.join(__dirname, "dev/assets/js/company.js"),
contact : path.join(__dirname, "dev/assets/js/contact.js"),
privacy : path.join(__dirname, "dev/assets/js/privacy.js"),
},
output: {
path: path.join(__dirname, 'dist'),
filename: 'assets/js/[name].js'
}
}
:entry
webpackはJavaScriptを軸として構成されるため、動的な動作の有無にかかわらず1ページごとにjsファイルを作ります。
entryに複数記述することで各ページを設定できます。
:output
ビルド後のファイルはdistディレクトリに出力します。
filenameの[name]はentryのキー名を割り当てています。
JSのビルド
babel-loaderを使ってコンパイルします。またES6(ES2015)記述に対応するためbabel-preset-envを使って対応バージョンを指定します。
npm install --save-dev babel-core babel-loader babel-preset-env
// 抜粋
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
"presets": [
["env", {
"targets": {
"browsers": ["last 2 versions", "IE 11"]
}
}]
]
}
}
]
}
]
}
}
SASS(SCSS)のビルド
sass-loader,postcss-loader,css-loader,style-loaderの4つを使用します。
またCSSは外部ファイル化したいので、ExtractTextPluginを読み込んで処理しています。
ベンダープレフィクスの自動付与はpostcss-loaderでautoprefixerを動作させています。(設定は前述のpostcss.config.jsに記載しているのでここでは読み込むだけ)
css-loaderのオプションで圧縮とソースマップの無効を指定しています。
なおCSS内記述の画像ファイルのパス関係を調整するため、publicPathを指定しています。(詳細は後述)
npm install --save-dev sass-loader postcss postcss-loader css-loader style-loader node-sass extract-text-webpack-plugin autoprefixer
// 抜粋
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const autoprefixer = require("autoprefixer");
module.exports = {
module: {
rules: [
test: /\.scss$/,
use: ExtractTextPlugin.extract({
publicPath: '../../',
fallback: 'style-loader',
use: [{
loader : 'css-loader',
options: {
minimize: true,
sourceMap: false
}
},'sass-loader', 'postcss-loader']
})
},
]
},
plugins: [
new ExtractTextPlugin({
filename: 'assets/css/[name].css',
disable: false,
allChunks: true
}),
]
}
HTMLのビルド
HTMLは部品の共通化や変数を使用して構築するため、ejsテンプレートエンジンを使用します。(記述方法は後述)
htmlの各ページはpluginsのHtmlWebpackPluginで生成するため、ここで全ページ分の記述します。
(entryで複数記述ができるような感じで、こちらも変数で記述できるといいのですが…そういった記述は対応していないようなので断念。)
npm install --save-dev html-loader extract-loader ejs ejs-compiled-loader html-webpack-plugin
// 抜粋
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.ejs$/,
use: [
{
loader : 'extract-loader',
},
{
loader : 'html-loader',
},
{
loader : 'ejs-compiled-loader'
}
]
},
]
},
plugins: [
new HtmlWebpackPlugin({
inject: false,
filename : 'index.html',
template : 'dev/index.ejs',
})
new HtmlWebpackPlugin({
inject: false,
filename : 'products.html',
template : 'dev/products.ejs',
})
// (ページ数分記述します...) //
]
}
画像
HTMLやCSSの中に書かれた画像パスを読み取り、file-loaderでdistに出力します。
devと同じディレクトリ構成でdistに出力するため、nameに[path]でディレクトリ指定しています。
また相対パスの関係を調整するためにoutputPathで保存先パスを変換しています。
詳細は下記を参照。
file-loaderで画像を扱うときのパス指定
なお先にscssのところでパスを調整するためにpublicPathを記述していますが、ここのパスと関係してきます。
画像ファイルのパスは下記の通りここのoutputPathでdev/を削除したパス(=assets/img/...)で記述されますが、CSSファイル内記載の画像は
CSSディレクトリはHTMLファイル設置場所と2階層離れているため、scssのところでpublicPathを記述してパス関係を調整しています。
npm install --save-dev file-loader
// 抜粋
module.exports = {
module: {
rules: [
{
test: /\.(jpg|png|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[path][name].[ext]',
outputPath : path => path.replace(/^dev\//,''),
}
}
]
},
]
}
}
PHPファイル
phpはwebpack自身では何も処理しないので、devからdistへ受け渡すだけです。
// 抜粋
module.exports = {
module: {
rules: [
{
test: /\.php$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
}
}]
},
]
}
}
その他
- webpack.ProvidePluginプラグイン
生成するJSファイルにライブラリを追加します。
たとえばPromiseにes6-promiseを割り当てることで、Promiseが利用できないIE11などでも使えるよう機能追加されます。
npm install --save jquery es6-promise
// 抜粋
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
Promise : 'es6-promise',
}),
]
}
JSファイルの記述方法
JSファイルは通常の各ページでのjavaScript処理に加えて、対応するCSSファイルをwebpackで処理させるためにここで読み込ませます。
また、HTMLやCSSに記載はないもののJS内で使う画像などを読み込ませておきたい場合やPHPファイルもここで指定します。
import '../css/index.scss'
import '../images/loading.gif'
import 'functions.php'
/* ↓ ページ固有のjavaScript処理を記述 ↓ */
なおwebpack.configでresolveを指定しておくと指定した拡張子の記述を省略できます。
// 抜粋
module.exports = {
resolve: {
extensions: ['.js', '.css', '.scss'],
modules: [path.join(__dirname, "dev"), 'node_modules']
},
}
extensionsで省略したい拡張子を指定します。こちらを追記すると、先ほどのindex.jsは以下のように書けます。
import '../css/index'
import '../images/loading.gif'
import 'functions.php'
/* ↓ ページ固有のjavaScript処理を記述 ↓ */
…これだけだとほぼインパクトないですが。。ファイル数が増えてくると威力を発揮します。たぶん。
webpack.config.js
ここまでの記述をまとめると下記のようになります。
"use strict";
const path = require("path");
const webpack = require("webpack");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const autoprefixer = require("autoprefixer");
module.exports = {
devtool: 'inline-source-map',
entry: {
index : path.join(__dirname, "dev/assets/js/index.js"),
products : path.join(__dirname, "dev/assets/js/products.js"),
company : path.join(__dirname, "dev/assets/js/company.js"),
contact : path.join(__dirname, "dev/assets/js/contact.js"),
privacy : path.join(__dirname, "dev/assets/js/privacy.js"),
},
output: {
path: path.join(__dirname, 'dist'),
filename: 'assets/js/[name].js'
},
resolve: {
extensions: ['.js', '.css', '.scss'],
modules: [path.join(__dirname, "dev"), 'node_modules']
},
module: {
rules: [
{
test: /\.ejs$/,
use: [
{
loader : 'extract-loader',
},
{
loader : 'html-loader',
},
{
loader : 'ejs-compiled-loader',
}
]
},
{
test: /\.php$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
}
}]
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: "css-loader",
})
},
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [{
loader : 'css-loader',
options: {
minimize: true,
sourceMap: false
}
},'sass-loader', 'postcss-loader']
})
},
{
test: /\.(jpg|png|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[path][name].[ext]',
outputPath : path => path.replace(/^dev/,''),
}
}
]
},
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
"presets": [
["env", {
"targets": {
"browsers": ["last 2 versions", "IE 11"]
}
}]
]
}
}
]
}
]
},
plugins: [
new webpack.optimize.UglifyJsPlugin(),
new HtmlWebpackPlugin({
inject: false,
filename : 'index.html',
template : 'dev/index.ejs',
}),
new HtmlWebpackPlugin({
inject:false,
filename : 'products.html',
template: 'dev/products.ejs',
}),
new HtmlWebpackPlugin({
inject:false,
filename : 'company.html',
template: 'dev/company.ejs',
}),
new HtmlWebpackPlugin({
inject:false,
filename : 'contact-input.html',
template: 'dev/contact-input.ejs',
}),
new HtmlWebpackPlugin({
inject:false,
filename : 'contact-complete.html',
template: 'dev/contact-complete.ejs',
}),
new HtmlWebpackPlugin({
inject:false,
filename : 'privacy.html',
template: 'dev/privacy.ejs',
}),
new ExtractTextPlugin({
filename: 'assets/css/[name].css',
disable: false,
allChunks: true
}),
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
Promise : 'es6-promise',
}),
]
};
package.json
必要なnpmパッケージは最終的にはこんな感じ。(2018/2現在のバージョン)
小規模なサイトという前提なので、まだ現役だと思われるjqueryもdependenciesに入れていますが、ここはお好みで。。
{
"name": "(project name)",
"version": "(x.x.x)",
"description": "(説明文)",
"dependencies": {
"es6-promise": "^4.2.2",
"jquery": "^3.2.1",
"reset-css": "^2.2.1",
},
"devDependencies": {
"autoprefixer": "^8.0.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"css-loader": "^0.28.7",
"ejs": "^2.5.7",
"ejs-compiled-loader": "^1.1.0",
"extract-loader": "^1.0.1",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.5",
"html-loader": "^0.5.1",
"html-webpack-plugin": "^2.30.1",
"node-sass": "^4.7.2",
"postcss": "^6.0.14",
"postcss-loader": "^2.0.9",
"sass-loader": "^6.0.6",
"style-loader": "^0.20.1",
"webpack": "^3.10.0"
},
"scripts": {
"build": "webpack --watch --progress --config webpack.config.js"
},
"author": "(author name)",
"license": "ISC"
}
なおscriptsでbuildコマンドを定義しているので、下記コマンドで実行できます。
npm run build
watch指定も入れているので、関連ファイルの更新を検知して自動ビルドされます。
EJSテンプレートエンジンでパーツの共通化
ヘッダー、フッター、グローバルメニューなど、各ページ共通のコンテンツは部品化しておくと便利ですので、EJSテンプレートエンジンを使ってこれらのパーツを共通化します。また効率化のために共通変数も外部ファイル化します。
例として「head」と「footerコンテンツ」のHTMLソースと、「サイト名」と「description」を共通変数として外部共通ファイル化します。
なお共通ファイルは/dev/componentsディレクトリを作って保存する形とします。
/
├ dev/ (開発用ディレクトリ)
│ └ assets/
│ └ components/ (共通化パーツ格納用)
読み込み元ファイル
<% include dev/components/common %> <!--共通変数を読み込み -->
<%
var common = getData();
var variables = {
pageId : 'index'
title : 'ページタイトル'
};
%>
<!doctype html>
<html>
<% include dev/components/head%> <!-- ヘッダーを読み込み -->
<body>
<p>TOP PAGE CONTENTS</p>
<% include dev/components/footer %> <!--フッターコンテンツを読み込み -->
</body>
</html>
ページ固有の変数はそのページ自体に記述し、共通変数はcommon.ejsで定義した関数を読み込みgetData()でcommonに格納しています。
参考サイト:テンプレートエンジンEJSで使える便利な構文まとめ
共通ファイルは下記の通り。
<!-- 共通変数を定義して関数化 -->
<%
getData = function () {
return {
siteName : 'サイト名',
description : 'サイトの説明文です。サイトの説明文です。'
}
};
%>
<head>
<meta charset="UTF-8">
<meta name="description" content="<%= common.description %>">
<title><%= variables.title %> | <%= common.siteName %></title>
<link rel="stylesheet" type="text/css" href="assets/css/<%= variables.pageId %>.css">
</head>
<footer id="main-footer" class="area-footer">
<ul class="link-footer">
<li><a href="/privacy.html">個人情報の保護</a></li>
<li><a href="/contact.php">お問い合わせ</a></li>
</ul>
<div class="copyright">
<p>© 2018 company</p>
</div>
</footer>
<script src="assets/js/<%= variables.pageId %>.js"></script>
まとめ
webpackを使ってサイトごとビルドする良い実例がなかったので書いてみました。
誰か困っている方の参考になれば幸いです。
もし間違っている記述や認識、もっとこうしたほうがいいというアイデアがあればご指摘ください。