吉本集の個人ブログ
Web制作の技術について書いています。たまに日記も書きます。

タスクランナー(gulpなど)を使用せず、npm+webpackで構築する開発環境

2017年7月6日 / category : lab

今回はWeb制作(ホームページ制作)時の開発環境について記事を書きます。
確か2年前頃は、タスクランナーはGruntを使用していました。
1年前頃からは、gulpに切り替えて作業していました。
どちらも便利なタスクランナーですが、今年からは、タスクランナーを使用しない開発環境に変えています。
手法としては、npm-scriptsでwebpackを監視し、webpackでTypeScriptとScssのコンパイルを行います。
ファイルの自動リロードには、browser-syncを使用します。

まず、私の開発環境ですが、

webpack(webpack3) → npm-scriptsによって実行
ファイルの自動リロード → browser-syncを使用
html → ssiを使用しファイルを分割(include化)
css → SCSSをコンパイルして生成
js → TypeScriptをコンパイルして生成

という環境です。

それでは順番に解説していきます。
npm-scriptsの設定からです。

npm-scriptsでwebpack, browser-syncを実行する
まず、必要になる下記のパッケージをインストールします。

browser-sync → ファイルの自動リロード
browsersync-ssi → ssiを使用できるようにする
npm-run-all → npm-scriptsの実行を操作する
webpack → css,jsを生成するビルドツールとして使用

npm
npm install browser-sync --save-dev
npm install browsersync-ssi --save-dev
npm install npm-run-all --save-dev
npm install webpack --save-dev
package.json
{
  ・・・省略
  "devDependencies": {
    "browser-sync": "^2.18.12",
    "browsersync-ssi": "^0.2.4",
    "npm-run-all": "^4.0.2",
    "webpack": "^3.0.0"
  }
}

続いて、npm-scriptsの設定です。
npm initを実行してpackage.jsonを生成した場合、
scriptsに、”test”: “echo “Error: no test specified” && exit 1″と記述が入っているかと思いますが、ここに追記していきます。”test”コマンドは削除しても問題ありません。

package.json
{
  ・・・省略
  "scripts": {
    "watch:server": "browser-sync start --config bs-config.js",
    "watch:webpack": "webpack -w",
    "w": "npm-run-all -p watch:webpack watch:server"
  }
}

順に説明すると、

“watch:server”コマンド
browser-syncをstart(実行)させます。その際、configファイルとして、bs-config.jsに記述されている設定を見に行きます。

“watch:webpack”コマンド
webpackによるファイル変更の監視

“w”コマンド
“watch:webpack”コマンド、”watch:server”コマンドを並列実行

作業を開始する際に、”w”コマンドを実行することで、ブラウザが立ち上がり、ファイルを更新する度にブラウザがリロードされるようになりますが、そのための設定はこれから解説します。

“w”コマンドの実行は下記です。

npm
npm run w

bs-config.jsの設定
下記の実行でbs-config.jsを生成します。

npm
browser-sync init

上記の実行でエラーが起きる場合は、browser-syncをグローバル環境にインストールしていない可能性がありますので、
次の実行で、browser-syncをグローバル環境にインストールしてから再度、上記の実行を行ってみてください。

npm
npm install browser-sync -g

生成されたbs-config.jsを見ると色々と記述がありますが、私が普段使用している内容はとてもシンプルです。
bs-config.jsの中身を一度全て削除し、下記の内容にしてください。

bs-config.js
const ssi = require('./node_modules/browsersync-ssi');

module.exports = {
	"files": "./src/**/*.css, ./src/**/*.js, ./src/**/*.html",
	"server": {
		baseDir: "./src/",
		index: "index.html"
	},
	"proxy": false,
	"port": 3000,
	"middleware": ssi({
		baseDir: "./src",
		ext: ".html",
		version: "1.4.0"
	})
}

私の作業フォルダはsrcフォルダですので、パス関連は”./src/”から始まっています。
まず、先ほどインストールしたbrowsersync-ssiをssi変数に格納します。

“files”は、監視対象となるファイルを指定、
“server”は、パスの基準となるディレクトリを指定し、初期に表示されるhtmlファイルも指定しておきます。
“middleware”で、ssi変数に格納したbrowsersync-ssiの関数を設定することでssiを使用することができるようになります。

これでbs-config.jsの設定は完了です。(詳しい解説は割愛させていただきます。)
続いて、webpackの設定について解説します。

webpack.config.jsの設定
webpackのconfigファイルは、webpack.config.jsになります。
コマンドでの生成はできないようなので、自身で用意します。

まず、最初に定義するものとして、それぞれのディレクトリを変数に格納しておきます。

webpack.config.js
const PATH = require('path');
const SCSS_PATH = PATH.join(__dirname, './src/scss');
const CSS_PATH = PATH.join(__dirname, './src/css');
const TS_PATH = PATH.join(__dirname, './src/ts');
const JS_PATH = PATH.join(__dirname, './src/js');

PATH変数に、ファイルパスを扱うモジュールを格納し、
scssファイル、cssファイル、tsファイル、jsファイルのパスをそれぞれ変数に格納しておきます。

module.exportsは下記のようになります。

webpack.config.js
module.exports = [
  {
  	//scssファイルをコンパイルしてcssファイルを生成するための設定記述箇所
  },
  {
  	//tsファイルをコンパイルしてjsファイルを生成するための設定記述箇所
  }
];

scssファイルのコンパイル設定
まずは、scssファイルとcssファイルのディレクトリ構成ですが、

./src/scss/style.scss
./src/css/style.css ← 上のstyle.scssのコンパイルにより生成される

とします。
必要なパッケージは下記になります。

node-sass ← scssファイルのコンパイル
sass-loader ← scssファイルのローダー
css-loader ← cssファイルのローダー
extract-text-webpack-plugin ← cssファイルとして出力する

npm
npm install node-sass --save-dev
npm install sass-loader --save-dev
npm install css-loader --save-dev
npm install extract-text-webpack-plugin --save-dev

これらを使用したwebpack.config.jsの記述は下記になります。

webpack.config.js
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = [
	{
		entry: {
			'style': SCSS_PATH + '/style.scss'
		},
		output: {
			path: CSS_PATH,
			filename: '[name].css'
		},
		module: {
			rules: [
				{
					test: /.scss$/,
					use: ExtractTextPlugin.extract({
						use: ["css-loader", "sass-loader"]
					})
				}
			]
		},
		plugins: [
			new ExtractTextPlugin('[name].css')
		]
	}
];

細かい説明は割愛しますが、
まず、extract-text-webpack-pluginを使用するために、モジュールを変数に格納しておきます。

const ExtractTextPlugin = require(“extract-text-webpack-plugin”);

続いて、module.exportsの設定ですが、

entryに、対象となるscssファイルの指定、
outputに、出力されるcssファイルのディレクトリとファイル名を指定、
module:rules:testに、監視対象となるファイルの拡張子を指定、
module:rules:useに、使用するローダーを指定、
pluginsに、使用するプラグインを指定します。

この状態で、コマンド npm run w を実行すると、ブラウザが立ち上がり、style.scssファイルがコンパイルされ、style.cssファイルが、./src/css/の中に生成されるかと思います。

ただ、これだけでは実務レベルとはいきません。
cssファイルを生成する際に、チェックとルールを決めておきたいです。

csscombの設定
私が実務で重宝しているのが、csscombモジュールです。
cssファイルにコンパイルする際に、ルールを決めることができます。
csscombの説明は割愛しますが、webpack.config.jsへの記述のみ下記にまとめます。
csscombを使用するために必要なパッケージは下記になります。

csscomb ← csscombモジュール
csscomb-loader ← csscombのローダー

npm
npm install csscomb --save-dev
npm install csscomb-loader --save-dev
webpack.config.js
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = [
	{
		entry: {
			'style': SCSS_PATH + '/style.scss'
		},
		output: {
			path: CSS_PATH,
			filename: '[name].css'
		},
		module: {
			rules: [
				{
					test: /.scss$/,
					use: ExtractTextPlugin.extract({
						use: ["css-loader", "csscomb-loader", "sass-loader"]
					})
				}
			]
		},
		plugins: [
			new ExtractTextPlugin('[name].css')
		]
	}
];

続いて、ベンダープレフィックスの設定と、lintも入れてみます。
ベンダープレフィックスは、今回postcssを使用して設定してみます。
必要なパッケージは下記になります。

autoprefixer ← ベンダープレフィックスのモジュール
postcss-loader ← postcssのローダー
stylelint-webpack-plugin ← lintプラグイン

npm
npm install autoprefixer --save-dev
npm install postcss-loader --save-dev
npm install stylelint-webpack-plugin --save-dev

autoprefixerを使用するには、postcssのconfigファイルで設定する必要があります。
postcssについては今回説明を割愛します。
postcss.config.jsを生成し、下記の記述を追記してください。

postcss.config.js
module.exports = {
	plugins: {
		'autoprefixer': {}
	}
}

stylelint-webpack-pluginについては、lintする設定を.stylelintrcファイルに記述する必要があります。
こちらについても今回は説明を割愛します。

.stylelintrc
{
	"rules": {
		"string-quotes": "single",
		"color-hex-case": "lower",
		"color-hex-length": "short",
		・・・などなど
	}
}

これらをwebpack.config.jsに設定します。

webpack.config.js
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const StyleLintPlugin = require('stylelint-webpack-plugin');
module.exports = [
	{
		entry: {
			'style': SCSS_PATH + '/style.scss'
		},
		output: {
			path: CSS_PATH,
			filename: '[name].css'
		},
		module: {
			rules: [
				{
					test: /.scss$/,
					use: ExtractTextPlugin.extract({
						use: ["css-loader", "csscomb-loader", "postcss-loader", "sass-loader"]
					})
				}
			]
		},
		plugins: [
			new ExtractTextPlugin('[name].css'),
			new StyleLintPlugin()
		]
	}
];

module:rules:useのローダーにpostcss-loaderを追加します。
これでpostcss.config.jsで設定したautoprefixerを使用することができます。

stylelint-webpack-pluginをStyleLintPlugin変数に格納します。
pluginsに、new StyleLintPlugin()を追記し、.stylelintrcで設定したルールをチェックします。

この状態で、例えば、次のようなscssを記述すると、

style.scss
a{
	display: flex;
	&:after{
		content: "";
	}
}

下記のようなcssファイルが出力されるかと思います。

style.scss
a {
	display: -webkit-box;
	display: -ms-flexbox;
	display: flex;
}
a:after {
	content: '.';
}

これでscssファイルのコンパイル設定はルールチェック(統一)をし、autoprefixerの対応をし、プロパティもソートされ、
実務レベルまで対応できたかと思います。

画像、Webフォントなどのアセットの読み込みにもloaderが必要になります。
使用するloaderは、file-loaderです。

file-loader ← 画像、Webフォントなどのアセットの読み込みのローダー

npm
npm install file-loader --save-dev
webpack.config.js
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const StyleLintPlugin = require('stylelint-webpack-plugin');
module.exports = [
	{
		entry: {
			'style': SCSS_PATH + '/style.scss'
		},
		output: {
			path: CSS_PATH,
			filename: '[name].css'
		},
		module: {
			rules: [
				{
					test: /.scss$/,
					use: ExtractTextPlugin.extract({
						use: ["css-loader", "csscomb-loader", "postcss-loader", "sass-loader"]
					})
				},
				{
					test: /.(gif|jpg|png|svg|otf|ttf|woff|eot)$/,
					use: {
						loader: 'file-loader',
						options: {
							name: '[path][name].[ext]',
							emitFile: false
						}
					}
				}
			]
		},
		plugins: [
			new ExtractTextPlugin('[name].css'),
			new StyleLintPlugin()
		]
	}
];

optionsのnameに[path][name].[ext]を設定していますが、
実はここでまだ解決していない問題があります・・・。
私は普段からルートパス指定でパスを設定しますので、この問題にはぶつからず実務でも支障がないのですが、
例えば、scssファイル側で画像のパスを指定する際、

■style.scss

background-image: url('/img/home/title.png');

と指定し、コンパイルすると、

■style.css

background-image: url('/img/home/title.png');

上のように問題なくコンパイルされますが、パス指定を相対パスにした場合、

■style.scss

background-image: url('../img/home/title.png');

この指定でコンパイルすると、

■style.css

background-image: url('src/img/home/title.png');

と、srcフォルダからの指定になってしまいます。
この原因は、optionsのnameに[path][name].[ext]と指定しているためです。
ここで取得している値をそれぞれ見ると、

[path] → src/img/home/
[name] → title
[ext] → png

となります。
画像を格納しているimgフォルダ直下に全て画像が格納されるのであれば、nameの指定を[path][name].[ext]ではなく、

name: ‘../img/[name].[ext]‘

のような指定で解決できるのですが、実務ではimgフォルダ以下にさらにフォルダがあるケースはよくあります。
例えば、../img/home/title.pngのような画像パスを上の指定でコンパイルしてしまうと、

■style.scss

background-image: url('../img/home/title.png');

■style.css

background-image: url('../img/title.png');

となってしまい、画像の階層がおかしくなってしまいます。
相対パスにした場合のこの問題がまだ解決できていません。。。

以上が、scssファイルのコンパイルの設定になります。
続いて、TypeScriptのコンパイル設定です。

TypeScriptのコンパイル設定
まずは、tsファイルとjsファイルのディレクトリ構成ですが、
./src/ts/main.ts
./src/js/main.js ← 上のmain.tsのコンパイルにより生成される
とします。
必要なパッケージは下記になります。

typescript ← tsファイルのコンパイル
ts-loader ← tsファイルのローダー

npm
npm install typescript --save-dev
npm install ts-loader --save-dev
webpack.config.js
module.exports = [
	{
		entry: {
			'main': TS_PATH + '/main.ts'
		},
		output: {
			path: JS_PATH,
			filename: '[name].js'
		},
		module: {
			rules: [
				{
				test: /.ts$/
				use: ['ts-loader']
				}
			]
		}
	}
];

tsファイルのコンパイルの設定に関しては、シンプルです。
例えば、コンパイルされたjsファイルを圧縮したい場合は、

webpack.config.js
const webpack = require('webpack');
module.exports = [
	{
		entry: {
			'main': TS_PATH + '/main.ts'
		},
		output: {
			path: JS_PATH,
			filename: '[name].js'
		},
		module: {
			rules: [
				{
				test: /.ts$/
				use: ['ts-loader']
				}
			]
		},
		plugins: [
			new webpack.optimize.UglifyJsPlugin()
		]
	}
];

UglifyJsPlugin関数を使用して圧縮します。
これでtsファイルのコンパイルの設定が完了です。

htmlファイル、cssファイル、jsファイルに変更が入れば、browser-syncの自動リロード機能で、ページがリアルタイムに変更されます。
scssファイル、tsファイルに関しても、webpackの監視機能で変更が入るたびにcssファイル、jsファイルにコンパイルされます。

以上が、現在の私の開発環境となります。

最終的なwebpack.config.jsは、次の通りです。

webpack.config.js
const PATH = require('path');
const SCSS_PATH = PATH.join(__dirname, './src/scss');
const CSS_PATH = PATH.join(__dirname, './src/css');
const TS_PATH = PATH.join(__dirname, './src/ts');
const JS_PATH = PATH.join(__dirname, './src/js');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const StyleLintPlugin = require('stylelint-webpack-plugin');
const webpack = require('webpack');
module.exports = [
	{
		entry: {
			'style': SCSS_PATH + '/style.scss'
		},
		output: {
			path: CSS_PATH,
			filename: '[name].css'
		},
		module: {
			rules: [
				{
					test: /.scss$/,
					use: ExtractTextPlugin.extract({
						use: ["css-loader", "csscomb-loader", "postcss-loader", "sass-loader"]
					})
				},
				{
					test: /.(gif|jpg|png|svg|otf|ttf|woff|eot)$/,
					use: {
						loader: 'file-loader',
						options: {
							name: '[path][name].[ext]',
							emitFile: false
						}
					}
				}
			]
		},
		plugins: [
			new ExtractTextPlugin('[name].css'),
			new StyleLintPlugin()
		]
	},
	{
		entry: {
			'main': TS_PATH + '/main.ts'
		},
		output: {
			path: JS_PATH,
			filename: '[name].js'
		},
		module: {
			rules: [
				{
					test: /.ts$/,
					use: ['ts-loader']
				}
			]
		},
		plugins: [
			new webpack.optimize.UglifyJsPlugin()
		]
	}
]