SPAでない Web Application のための webpack

SPAでない Web Application のための webpack

SPAでない Web Application で webpack を使おうしたときに少しハマったの でメモしておきます。

何が問題だったか

TypeScript を使いたかったので、素直に webpack を使おうと思ったのですが、 React とか SPA での使い方(要するに一つの js ファイルに bundle する感じ)は 例が多いのですが、1 html に 1 js (ソースは 1 tsファイル) というパター ンはあまり見かけなかったので、少し試行錯誤しました。

ディレクトリ構成

だいたい以下のようなディレクトリ構成を想定しておきます。

  • src/ts/pageNN.ts に各ページに書く TypeScript のソースを置きます。
  • src/ts/common.ts (仮称)は、共通モジュールとなります。
  • コンパイル結果は public/js/pageNN.js に書き出すイメージです。
  • ただしOSSのモジュール等は、public/js/vendor.js に書き出し、pageNN.js には含みません。
  • デバッグ用に .map を出力しています。
  • CSS のソースは、src/css/base.css 他に置き、コンパイルしたら public/js/style.css.js に出力するイメージです。

    .
    ├── package-lock.json
    ├── package.json
    ├── public
    │     ├── page01.html
    │     ├── page02.html
    │     ├──    :
    │     └── js
    │         ├── page01.js
    │         ├── page01.js.map
    │         ├── page02.js
    │         ├── page02.js.map
    │         ├──    :
    │         ├──    :
    │         ├── style.css
    │         ├── style.css.js
    │         ├── style.css.js.map
    │         ├── style.css.map
    │         ├── vendor.js
    │         └── vendor.js.map
    ├── src
    │     ├── css
    │     │     ├── base.css
    │     │     ├──   :
    │     │     └──
    │     ├── js
    │     │     └── jquery.numeric.js
    │     └── ts
    │         ├── page01.ts
    │         ├── page02.ts
    │         ├──     :
    │         ├──     :
    │         └── common.ts
    ├── tsconfig.json
    ├── tslint.json
    └── webpack.config.js
    

webpack.config.js の設定

最終型に至るまでの過程を順を追って説明します。

複数ソース複数出力の設定

ページ毎にソースを分ける、ということを実現するには、entry: を分けて やればよいです。

※本当は、ページが増えたら自動的に entry も増える、といった構成がよい と思うのですが、webpack.config.js をあまり複雑にしたくなかったのでこれ で妥協しました。

module.exports = {
  entry: {
    'entry01': './src/ts/page01',
    'entry02': './src/ts/page02',
  },
  output: {
    path: path.resolve(__dirname, 'public/js'),
  },
  // :
};

このように記述することで、public/js/page01.js 等にコンパイル結果が出 力されます。

vendor.js への外部依存ソースの分離

このままだと、各ソースが依存(TypeScript で言うところの import) してい るソースは、上記 page01.js 等に各々書き出されてしまいます。

これはこれで悪くはないのですが、各 js ファイルが大きくなってしまうのが 気になるとか、どうせならキャッシュを効かせたい、等と考えるようになりま す。そこで、.ts が依存するソースは、vendor.js にひとまとめにしてしま います。

module.exports = {
  // :
  optimization: {
    splitChunks: {
      name: 'vendor',
      chunks: 'initial',
    }
  },
  // :
};

これにより public/js/vendor.js というファイルが生成されるようになる ので、HTML 側から忘れずに参照するようにしておきます。

TypeScript まわり

TypeScript 関連を以下に抜き出してみました。ここでの記載内容は、SPA の 場合とあまりかわらないはずです。

module.exports = {
  // :
  module: {
    rules: [
      {
        test: /\.ts$/,
        // exclude: /node_modules/,
        use: {
          loader: 'ts-loader',
        }
      },
      // :
    ]
  },
  resolve: {
    extensions: ['.ts', '.js']
  },
  // :
};

CSS まわり(Sass の導入)

CSS については、Sass を使うことにします。また css ソースは、複数ソース に分かれていることを想定し、複数ソース→1ファイルに変換する、というイ メージで考えてみます。まずは entry への追加です。

const glob = require('glob');
const cssfiles = glob.sync('src/**/*.css').map(f => `./${f}`);

module.exports = {
  entry: {
    'entry01': './src/ts/page01',
    'entry02': './src/ts/page02',
    // :
    'style.css': cssfiles
  },
  // :
};

ここでは対象となる css を glob でかき集めていますが、./ を先頭につけ る必要があったので、上記のような書き方になっています。

const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractCSS = new ExtractTextPlugin({ filename: '[name]', allChunks: false });

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: extractCSS.extract({ use: 'css-loader' })
      },
      {
        test: /\.scss$/,
        use: extractCSS.extract({ use: [{ loader: 'css-loader' }, 'sass-loader'] })
      },
      {
        test: /\.(png|gif|svg|woff)$/,
        use: 'file-loader'
      }
    ]
  },
  plugins: [
    extractCSS
  ],
  resolve: {
    extensions: ['.css', '.png', '.gif', '.svg']
  },
  // :
};

デバッグ関連の設定

実はここでもハマりました。ざっくりデバッグ関連の設定を抜き出しておきま す。

const DEBUG = !process.argv.includes('production');
module.exports = {
  // :
  devtool: DEBUG ? 'source-map' : false,
  devServer: {
    contentBase: path.resolve(__dirname, 'public'),
    publicPath: '/js/',
    port: 3000,
    watchContentBase: true
  }

devtool: source-map でソースマップを出力します。あと上記 devServer の設定が結構デリケートでした(contentBase, publicPath, watchContentBase, のどれかひとつでも間違えると、livereload してくれま せん。しかもなんのエラーもなく手がかりもない…)。

まとめ

ここまでの結果をまとめておきます。

  • webpack.config.js

    const path = require('path');
    const glob = require('glob');
    const ExtractTextPlugin = require('extract-text-webpack-plugin');
    const extractCSS = new ExtractTextPlugin({ filename: '[name]', allChunks: false });
    const cssfiles = glob.sync('src/**/*.css').map(f => `./${f}`);
    const DEBUG = !process.argv.includes('production');
    
    module.exports = {
    entry: {
    'entry01': './src/ts/page01',
    'entry02': './src/ts/page02',
    'style.css': cssfiles
    },
    output: {
    path: path.resolve(__dirname, 'public/js'),
    },
    optimization: {
    splitChunks: {
      name: 'vendor',
      chunks: 'initial',
    }
    },
    module: {
    rules: [
      {
        test: /\.ts$/,
        // exclude: /node_modules/,
        use: {
          loader: 'ts-loader',
        }
      },
      {
        test: /\.css$/,
        use: extractCSS.extract({ use: 'css-loader' })
      },
      {
        test: /\.scss$/,
        use: extractCSS.extract({ use: [{ loader: 'css-loader' }, 'sass-loader'] })
      },
      {
        test: /\.(png|gif|svg|woff)$/,
        use: 'file-loader'
      }
    ]
    },
    plugins: [
    extractCSS
    ],
    resolve: {
    extensions: ['.ts', '.js', '.css', '.png', '.gif', '.svg']
    },
    devtool: DEBUG ? 'source-map' : false,
    devServer: {
    contentBase: path.resolve(__dirname, 'public'),
    publicPath: '/js/',
    port: 3000,
    watchContentBase: true
    }
    };
    
  • html

    <html>
    <head>
    <!--
      head here
    -->
    <link href="js/style.css" rel="stylesheet" type="text/css" />
    <script type="text/javascript" src="js/vendor.js"></script>
    </head>
    <body>
    <!--
      body here
    -->
    <script type="text/javascript" src="js/page01.js"></script>
    </body>
    </html>
    

今後は SPA でアプリを作ることが多くなりそうですが、もしかしたら使うこ ともあるかもしれないので、まとめておきました。

comments powered by Disqus