Webpack4でMPAの開発環境構築。HTML, SCSS, JavaScript, TypeScript

Tatsuya Asami
18 min readMay 5, 2020

--

【所要時間】

TypeScript, JavaScript合わせて15時間くらい

【レポジトリ】

templateとして使ってください。そして何かあればコメント下さい。フォークしてプルリク作ってくれるとありがたいです。

  • JavaScriptバージョン
  • TypeScriptバージョン

【概要】

  • フレームワークを使わないJavaScript, TypeScriptの開発環境構築。
  • 私が心を込めて1つずつパッケージを追加しました。

【目次】

  • コンセプト
  • 作成した開発環境
  • TypeScriptをJavaScriptに変換する際に、ts-loaderを使うかbabel-loaderを使うか
  • css, 画像ファイルなどをjsにインジェクトするか、外部ファイルにするか
  • 画像などの静的ファイルの設定
  • パフォーマンスチューニング
  • ハッシュ化

【関連記事】

コンセプト

案件を受注する前に実際に動くものをお客さんにみてもらうためにモックを作ることがよくあります。それはフロントエンドエンジアではなく、営業や手の空いているバックエンドエンジニアが作ることもあります。(営業含めてほぼ全員がプログラマーとしての経験が多少ある会社に勤務しています。)

また、受注してからも要件定義、設計、実装といったウォーターフォール開発ではなく、とりあえず最初の機能を実装。もし気にってもらえたら引き続き機能追加・・・といった展開になることもよくあります。

このようにしてとりあえず見せるように作ったが、だんだんと継ぎ足しで実装を進めていき、いざ腰を据えて実装する時にはまあまあの規模に。

そんな時にありがちなのが

  • 環境構築をする余裕もなくとりあえず進めているから、フォーマットはバラバラ。
  • 最初は当然APIは存在しないので仮実装を進めるが、その仮のデータがベタで書かれている
  • 今度はバックエンドも実装が始まったけど、dotEnvとかも用意していないからURLを下手で書いている

などなど、とりあえず動くものを作らなきゃいけないから後回しになりがちです。これは最初から設定されてれば解消できている可能性が高いです。

というわけでフロントエンドエンジア以外でもとりあえず実装を進められる状態まで用意したレポジトリを作りました。

作成した開発環境

HTML, SCSS, JavaScript(またはTypeScript)で実装できる環境 + dotEnv + axios + json-server

TypeScriptは急いでる時にフロントに慣れていない人が使うか?と思いましたが、過去にCやC++を専門としている人がフロントを実装した時にTypeScriptで実装していたので、一応用意しました。

SCSSは初めての人でも何となく書き始められるし、入れるデメリットもないので入れました。CSSも一応対応しています。

dotenv-webpack

.envが用意されていればAPIのURLなど、環境ごとに変わるものはそこに記載するでしょう。用意されていないからベタで書いてしまうのです。あればそこに書いて解決です。今回はenvファイルを複数用意できるように設定しています。

axios

HTTPリクエストをする時に使用するライブラリ。どのプロジェクトでも必ず使用しているので今回も使用します。ヘッダー情報などを共通で設定できたり、入れるデメリットもないと思っています。

json-server

これも重要です。ローカルで簡単にモックサーバーを建てられます。jsonを記載するだけで、モックサーバーが用意できます。用意されていないからベタで書いてしまうのです。あればそこに書いて解決です。
APIとやりとりするであろう値をjsonに書いていると、バックエンドの実装ができた際も移行がスムーズです。TypeScriptの場合型指定の問題も出てくるんですが、それを考慮してもローリスクハイリターンです。

環境構築関連で色々入れなければならなくて、devDependenciesがこんなに・・・

TypeScriptをJavaScriptに変換する際に、ts-loaderを使うかbabel-loaderを使うか

babel-loaderでjsにトランスパイルし、型チェックは fork-ts-checker-webpack-plugin で行うことにしました。

  • tsconfigでトランスパイルするJSの形式が指定できるが、不完全な場合もありそう。
  • babel-loaderならばJSのトランスパイルに融通が利くが、型チェックはできない。
  • babelなら対応ブラウザの設定を browserslist に任せられる。cssのautoprefixerもbrowserslist に任せられるので、対応ブラウザの設定が1箇所で済む

などを配慮し、以下の2択になりました。

  • ts-loaderでJSに変換、JSはbabel-loaderで変換する。
  • babel-loaderで変換、型チェックは別でする。

そしてts-loaderを使っている場合も、ts-loaderではトランスパイルだけを行、fork-ts-checker-webpack-plugin で型チェックを行った方が実行速度が早いというのもどこかで読みました。
それであればts-loaderを挟む必要はないという判断です。他に似たようなことをやっている人があまりいない(babelでトランスパイルするが、型チェックはtscで行っている人が多い)ので不安です。

fork-ts-checker-webpack-plugin で型チェックを行うので、ビルド時に型エラーがあればちゃんとエラーになります。webpack-dev-serverでは起動時に型エラーがあるとローカルサーバーは起動しませんが、既に立ち上げている状態で型エラーが出ても画面は表示されるので、実装しやすいです。

ただ型エラーがwebpack-dev-serverの記述で隠れてしまうので、ちょっと型エラーが発見しにくくなります。何かいい方法があれば知りたいです。

// webpack.common.jsconst path = require('path');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = ({ outputFile, assetFile, envFilePath, assetPath }) => {
return {
entry: {
...
},
output: {
...
},
plugins: [
// 型チェック
new ForkTsCheckerWebpackPlugin(),
...
],
module: {
rules: [
{
enforce: 'pre',
test: /\.(ts|js)$/,
use: 'eslint-loader',
exclude: /node_modules/,
},
{
test: /\.(ts|js)$/,
// tsからjsの変換はbabel-loaderで行う
// 型チェックはForkTsCheckerWebpackPluginで行う
use: 'babel-loader',
exclude: /node_modules/,
},
...

CSS, 静的ファイルを個別ファイルとして出力するかjsにインジェクトするか

個別ファイルとして出力することにしました。

あまりパフォーマンスの難しいことはわからないのですが、

  • 個別ファイルの方がわかりやすい。
  • 変更がなかった場合にキャッシュを使用できる。

という理由から、個別ファイルの方がメリットがあると考えました。
cssでstyle-loaderを使うとタグにインジェクトされるので、cssの変更しかない場合やその逆の場合も新しいファイルが生成されてしまい、キャッシュを使えないのでは?と思ったからです。

正直パフォーマンスの部分ではそれなりの規模にならないとあまり変わらないと思っているので、そんなに気にしていません。(気にするような規模、案件ならjsもフレームワークを使ったSPAで実装していると思う)

静的ファイルの設定

ここはフロントだけではなくインフラとの調整が必須になる部分。buildして生成された静的ファイルをどこに保存するかで、フロントが画像を読みにいくURLを変える必要が出てくる。ローカル開発環境では特に調整する必要はない。

// webpack.common.jsmodule.exports = ({ outputFile, assetFile, envFilePath, assetPath }) => {         ...        {
// 他の種類の静的ファイルを使用する場合は同様の記述で追加する。
test: /\.(png|svg|jpe?g|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: `${assetFile}.[ext]`,
outputPath: 'assets/images/',
// 画像保存先によってパスを変更する。
publicPath: `${assetPath}assets/images/`,
},
},
],
},
{
test: /\.(ttf|woff2?)$/,
use: [
{
loader: 'file-loader',
options: {
name: `${assetFile}.[ext]`,
outputPath: 'assets/fonts/',
publicPath: `${assetPath}assets/fonts/`,
},
},
],
},

assetPathはwebpack.dev.jswebpack.prod.jsから渡される。

const path = require('path');
const webpackMerge = require('webpack-merge');
const commonConfig = require('./webpack.common');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
const OptimizeCssPlugin = require('optimize-css-assets-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const outputFile = '[name].[chunkhash]';
const assetFile = '[name].[contenthash]';
// 静的ファイルを保存する場所によって変える。インフラ担当者と要相談。
const assetPath = '/';
module.exports = (env) => { ... return webpackMerge(
commonConfig({ outputFile, assetFile, envFilePath, assetPath }),
{
mode: 'production',
plugins: createHtmlPlugins(
commonConfig({ outputFile, assetFile, envFilePath, assetPath})
...

同一ドメインに静的ファイルも保存されるならばこのままで良いはず。
ビルド後のファイルで動作確認をするならば、vscodeのliveserverを使うと良い。、このレポジトリでは dist/ がルートになるようにvscodeの settings.jsonも共有している。

パフォーマンスチューニング

パフォーマンスチューニングの最適な設定は事前に決めることは難しいと思うので、デフォルトの設定に寄せている。あとそもそもあまり気にしていない。webpackもchunkの設定はデフォルトで大抵は大丈夫だろうと言っている。

cssを最適化するためにoptimize-css-assets-webpack-pluginを使用しているが、optimizationを変更すると、productionモードのデフォルトで設定されていたhtmlやjsの最適化がリセットされてしまうので、terser-webpack-pluginを追加し、改めて指定し直している。
また、本番環境ではコンソールを削除するようにしている。

// webpack.prod.jsconst path = require('path');
const webpackMerge = require('webpack-merge');
const commonConfig = require('./webpack.common');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
const OptimizeCssPlugin = require('optimize-css-assets-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const outputFile = '[name].[chunkhash]';
const assetFile = '[name].[contenthash]';
// 静的ファイルを保存する場所によって変える。インフラ担当者と要相談。
const assetPath = '/';
module.exports = (env) => {
// package.jsonのscriptで --env.envFile=で指定されたパスのenvFileを使用する。
// 指定されていない場合は.env.productionを使用する
const envFilePath = env ? `./env/.env.${env.file}` : './env/.env.production';
// webpack.common.jsのentryで追加したhtmlファイルを動的に生成する。
const createHtmlPlugins = (entry) => {
// 最初にdistディレクトリを空にする
const htmpPlugins = [new CleanWebpackPlugin()];
Object.keys(entry).forEach((key) => {
htmpPlugins.push(
new HtmlWebpackPlugin({
template: path.resolve(__dirname, `./src/pages/${key}.html`),
// 出力されるファイル名
filename: `./pages/${key}.html`,
// headにjsファイルを入れたい場合はheadを指定
inject: 'body',
minify: {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true,
},
// 読み込むjsファイルを指定
chunks: [key],
})
);
});
return htmpPlugins;
};
return webpackMerge(
commonConfig({ outputFile, assetFile, envFilePath, assetPath }),
{
mode: 'production',
plugins: createHtmlPlugins(
commonConfig({ outputFile, assetFile, envFilePath, assetPath }).entry
),
optimization: {
minimizer: [
// javascriptの最適化
new TerserWebpackPlugin({
terserOptions: {
// consoleを削除する
compress: { drop_console: true },
},
}),
// cssの最適化
new OptimizeCssPlugin(),
],
},
}
);
};

ハッシュ化

ファイル名は webpack.dev.jswebpack.prod.js でそれぞれ指定したものを webpack.common.js に渡すことで、極力webpack.common.js で扱う共通処理を増やしている。

  • 開発環境ではハッシュ化しない。パスを見やすくするため。
  • 本番環境ではハッシュ化する。キャッシュを読みにいかない様にするため。

ハッシュの種類としては

にした。

// webpack.prod.jsconst outputFile = '[name].[chunkhash]';
const assetFile = '[name].[contenthash]';

chunkHashはチャンク毎にハッシュが生成され、contenthashはcontent毎にハッシュが生成される。ビルドした際に変化があったファイルのみ再生成されるので、ブラウザはキャッシュを読みに行きパフォーマンスがよくなる。

【感想】

今のところコードだけ書けば実装が進められる状態になっている。パフォーマンスチューニングについてもドキュメントをかなり漁ったが、ある程度の規模のアプリを作らないとそんなに差異が出ないと思われる。
今回のコンセプトである、「とりあえず実装を進められる用意をする」という意味では少し外れるが、色々勉強になった。

--

--

Tatsuya Asami
Tatsuya Asami

Written by Tatsuya Asami

Front end engineer. React, TypeScript, Three.js

No responses yet