ノウハウ・TIPS

vuejs-template/webpackをwebpack3からwebpack4へアップデートする方法について

ご無沙汰しております、アシアル笹亀です。
先月の10月8日にPHP Conference Japan 2023がオフラインイベントとして開催されました。PHP Conferenceといえば、いつもの大田区産業プラザPiOでの開催です。久しぶりのオフラインイベントで私も参加させていただきましたが、人も多くてPHP人気は健在だと感じました^^弊社も会計担当として開催のサポートさせていただきました。

https://phpcon.php.gr.jp/2023/

webpackとは

webpackはJavascriptのNode.js環境向けのモジュールハンドラーです。CSSやJavascript、画像などWebサイトを構成するファイルを1つにまとめるモジュールハンドラーのことです。
webpackを利用して複数ファイルをバンドルすることで利点を得ることができます。
 ・依存関係の解決
 ・リクエスト回数を減らす
 ・開発作業の分担がしやすくなる
Javascriptのフレームワークを利用したWebアプリケーションやハイブリットアプリの構築などでも利用されることも多いとおもいます。webpackの後継と言われている「Turbopack」も2022年10月頃に発表がありましたが、まだまだwebpackも現役で利用がされております。

webpack3からwebpack4へのアップデートの経緯

webpack3は2017年頃にリリースされたバージョンで長年運用しているWebアプリケーションがあり、アプリケーション内でNode.jsのバージョンアップが必要となり、webpack3がアップデートするバージョンで非対応となるため、webpack4へアップデートが必須要件となり、webpackについても4へアップデートをすることになりました。

webpack4へするにあたってのwebpack3のパッケージの整理

webpack3から4に移行する際の関連するビルド実施時に関するパッケージのバージョン情報などの変更点を調査し、洗い出す作業をおこないました。

パッケージwebpack3webpack4備考
webpack3.8.14.32.2webpackの本体のアップデート
webpack-cli-3.3.2
webpack-bundle-analyzer2.9.13.3.2webpack4対応のものにアップデート
webpack-dev-server-3.4.1ローカルサーバ用ライブラリ
webpack-dev-middlewareのバージョンアップにて
必要になったため追加
webpack-merge4.1.14.2.1webpack4対応のものにアップデート
html-webpack-plugin2.30.13.2.0HTML書き出しライブラリ
extract-text-webpack-plugin3.0.2-webpack4にて仕様が非推奨になった
mini-css-extract-plugin-0.6.0extract-text-webpack-pluginのかわりのもの
optimize-css-assets-webpack-plugin3.2.05.0.1webpack4対応のものにアップデート
uglifyjs-webpack-plugin-2.2.0webpack3のときは同梱されたいたが、
webpack4からはplugin化されて、
optiomization.minimizer配下に変更
webpack関連プラグインアップデート情報

上記にて洗い出した情報をもとにpackage.jsonの更新を行い、npm installにてアップデートしたパッケージをインストールし、webpack4でのビルド実施前の準備をおこないました。

buildコマンドのアップデート

私がアップデートしたwebpackのWebアプリケーションではVuejsを利用しているのですが、そちらのテンプレートにwebpackビルド用に準備されたており、そちらを利用しておりました。

あるあるの話になるのですが、テンプレートで用意されていたwebpack3に対してのビルド処理となっており、こちらが継続的なメンテナンスがされておらず、webpack4に対応する方法や対応されたアップデートのバージョンがありませんでした。いろいろと情報を駆使して探しておりますと、中国版のgithubであるgiteeにvuejs-tempateのwebpackをwebpack4に対応した情報を見つけました。中国語になっているので細かいところはわからないですが、ファイル構成などは同じだったのと、READMEに変更点が記載があり、修正をおこなえました。

vuejs-template/webpackをwebpack4へ修正対応については、vue-webpack4-templateの内容を参考に以下の手順にて実施をおこないました。

  1. build/utils.jsのwebpack3からwebpack4へ対応するための修正を適応する
    1. mini-css-extract-pluginプラグインを読み込み設定と以前のextract-text-webpack-pluginプラグインを削除
    2. vue-style-loaderのからMiniCssExtraPluginのloaderに変更
  2. build/webpack.prod.conf.jsのwebpack3からwebpack4へ対応するための修正を適応する
    1. mini-css-extract-pluginプラグインを読み込み設定と以前のextract-text-webpack-pluginプラグインを削除
    2. uglifyjs-webpack-pluginプラグインとvue-loaderプラグインの読み込み設定を追加
    3. utilsのstyleLodersのusePostCSSの設定を追加
    4. new UglifyJsPluginとnew OptimizeCSSPluginの部分をコメントアウトして、optimizationの中に設定の記載を追加
    5. HtmlWebpackPluginのchunksSortModeの設定をコメントアウト
    6. HtmlWebpackPluginにあるwebpack.optimize.CommonsChunkPluginの設定を削除
    7. VueLoaderPlugin、webpack.HashedModuleIdsPlugin、webpack.optimize.ModuleConcatenationPluginの生成を追加
    8. optimizationにsplitChunksの設定を追加

■build/webpack.prod.conf.js

var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
// var ExtractTextPlugin = require('extract-text-webpack-plugin')
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var UglifyJsPlugin = require('uglifyjs-webpack-plugin')
var VueLoaderPlugin = require('vue-loader/lib/plugin')

var env = config.build.env

var webpackConfig = merge(baseWebpackConfig, {
  watch: process.env.WEBPACK_WATCH === 'true',
  module: {
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true,
      usePostCSS: true
    })
  },
  devtool: config.build.productionSourceMap ? '#source-map' : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'),
  },
  plugins: [
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    new webpack.DefinePlugin({
      'process.env': env
    }),
    // new UglifyJsPlugin({
    //   uglifyOptions: {
    //     compress: {
    //       warnings: false
    //     }
    //   },
    //   sourceMap: config.build.productionSourceMap,
    //   parallel: true
    // }),
    // extract css into its own file
    new MiniCssExtractPlugin({
      filename: utils.assetsPath('css/[name].css'),
      chunkFilename: utils.assetsPath('css/[name].[contenthash].css')
    }),
    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    // new OptimizeCSSPlugin({
    //   cssProcessorOptions: {
    //     safe: true
    //   }
    // }),
    // generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: config.build.index,
      template: 'index.html',
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      }
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      // chunksSortMode: 'dependency'
    }),
    new HtmlWebpackPlugin({
      filename: config.build.blank,
      template: 'blank.html',
      inject: false,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
      }
      // chunksSortMode: 'dependency'
    }),
    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      }
    ]),
    // vue loader v15
    new VueLoaderPlugin(),
    // keep module.id stable when vendor modules does not change
    new webpack.HashedModuleIdsPlugin(),
    // enable scope hoisting
    new webpack.optimize.ModuleConcatenationPlugin()
  ],
  optimization: {
    runtimeChunk: {
      name: 'manifest'
    },
    minimizer: [
      new UglifyJsPlugin({
        cache: true,
        parallel: true,
        sourceMap: config.build.productionSourceMap,
        uglifyOptions: {
          warnings: false
        }
      }),
      new OptimizeCSSPlugin({
        cssProcessorOptions: config.build.productionSourceMap
          ? { safe: true, map: { inline: false } }
          : { safe: true }
      })
    ],
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      name: false,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'initial',
          priority: -10
        }
      }
    }
  },
  stats: 'verbose'
})

if (config.build.productionGzip) {
  var CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  )
}

if (config.build.bundleAnalyzerReport) {
  var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = webpackConfig

■buils/utils.js

var path = require('path')
var config = require('../config')
// var ExtractTextPlugin = require('extract-text-webpack-plugin')
var MiniCssExtractPlugin = require('mini-css-extract-plugin')

exports.assetsPath = function (_path) {
  var assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory
  return path.posix.join(assetsSubDirectory, _path)
}

exports.cssTestLoaders = function (options) {
  options = options || {}

  var cssLoader = {
    loader: 'css-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  // generate loader string to be used with extract text plugin
  function generateLoaders (loader, loaderOptions) {
    var loaders = [cssLoader]
    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })

      // load sass resources
      if (loader === 'sass') {
        loaders.push({
          loader: 'sass-resources-loader',
          options: {
            resources: [
              path.resolve(__dirname, '../src/assets/styles/_colors.scss'),
              path.resolve(__dirname, '../src/assets/styles/_mixins.scss'),
              path.resolve(__dirname, '../src/assets/styles/_sizes.scss'),
              path.resolve(__dirname, '../src/assets/styles/_vars.scss')
            ]
          }
        })
      }
    }

    // Extract CSS when that option is specified
    // (which is the case during production build)
    if (options.extract) {
      // return ExtractTextPlugin.extract({
      //   use: loaders,
      //   fallback: 'vue-style-loader'
      // })
      return [MiniCssExtractPlugin.loader].concat(loaders)
    } else {
      return ['vue-style-loader'].concat(loaders)
    }
  }

  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
  return {
    css: generateLoaders(),
    postcss: generateLoaders(),
    less: generateLoaders('less'),
    sass: generateLoaders('sass', { indentedSyntax: true }),
    scss: generateLoaders('sass'),
    stylus: generateLoaders('stylus'),
    styl: generateLoaders('stylus')
  }
}

exports.cssLoaders = function (options) {
  options = options || {}

  var cssLoader = {
    loader: 'css-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  // generate loader string to be used with extract text plugin
  function generateLoaders(loader, loaderOptions) {
    var loaders = [cssLoader]
    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })

      // load sass resources
      if (loader === 'sass') {
        loaders.push({
          loader: 'sass-resources-loader',
          options: {
            resources: [
              path.resolve(__dirname, '../src/assets/styles/global.scss')
            ]
          }
        })
      }
    }

    // Extract CSS when that option is specified
    // (which is the case during production build)
    if (options.extract) {
      // return ExtractTextPlugin.extract({
      //   use: loaders,
      //   fallback: 'vue-style-loader'
      // })
      return [MiniCssExtractPlugin.loader].concat(loaders)
    } else {
      return ['vue-style-loader'].concat(loaders)
    }
  }

  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
  return {
    css: generateLoaders(),
    postcss: generateLoaders(),
    less: generateLoaders('less'),
    sass: generateLoaders('sass', { indentedSyntax: true }),
    scss: generateLoaders('sass'),
    stylus: generateLoaders('stylus'),
    styl: generateLoaders('stylus')
  }
}

// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
  var output = []
  var loaders = exports.cssLoaders(options)
  for (var extension in loaders) {
    var loader = loaders[extension]
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }
  return output
}

exports.styleTestLoaders = function (options) {
  var output = []
  var loaders = exports.cssTestLoaders(options)
  for (var extension in loaders) {
    var loader = loaders[extension]
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }
  return output
}

その他、build時に問題があった箇所を修正

buildコマンドを実行している中でwebpackの処理が完了して、buildされた情報も出力されており、正常に終了しているのも関わらずbuildのプロセスが終了しないということが発生しました。少し強引ですが、process.exit(0)にてプロセスを強制的に正常終了をさせることで対策しました。

■build/build.js

require('./check-versions')()

process.env.NODE_ENV = 'production'

var ora = require('ora')
var rm = require('rimraf')
var path = require('path')
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../config')
var webpackConfig = require('./webpack.prod.conf')
var watch = require('watch')
var execa = require('execa')
var notifier = require('node-notifier')

var notifierTimeout = 5
var errorNotifierTimeout = 15

var spinner = ora('building for production...')
spinner.start()

rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, function (err, stats) {
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false,
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))

    if (stats.hasErrors()) {
      notifier.notify({
        title: 'Test build result.',
        message: 'Error webpack.',
        type: 'error',
        timeout: errorNotifierTimeout
      })
    } else {
      console.log(chalk.cyan('  Complete build. ( ' + new Date() + ' )\n'))
      notifier.notify({
        title: 'Test build result.',
        message: 'Complete build.',
        timeout: notifierTimeout
      })
    }
    process.exit(0)
  })
})

ローカル上での開発のためにwebpackのserver(build/dev-server.js)を立ち上げて動作確認をするのに利用しており、そちらに関する設定もwebpack4で動作するように修正をしました。修正した箇所はVueLoaderPluginを読み込んでクラスを生成をすることで動作しました。

build/webpack.dev.conf.js

var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
var VueLoaderPlugin = require('vue-loader/lib/plugin')

// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
  baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})

module.exports = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
  },
  // cheap-module-eval-source-map is faster for development
  devtool: '#cheap-module-eval-source-map',
  plugins: [
    new webpack.DefinePlugin({
      'process.env': config.dev.env
    }),
    // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),
    new FriendlyErrorsPlugin(),
    // vue loader v15
    new VueLoaderPlugin()
  ]
})

webpack4でのビルド

記載をさせていただいた内容をひとつひとつbuildコマンドを実行しながら確認を進めておりました。buildするたびに違うエラーが発生をしては修正を繰り返すという対応をしておりました。最終的には無事にアップデート作業を完了し「Build complete.」が表示されたときは苦労がかたちになったということでホッとしました。

webpack4のBuild complete.

まとめ

実際にwebpack3からwebpack4への対応をしていきましたが、webpackに関連したプラグインのバージョンアップや必要になるプラグインの追加、非対応になったプラグインの削除などについて、確認や判断するのがとても苦労しました。webpackがすでにバージョンが5となっていることもあり、すべて最新にしても動作しなく、ひとつずつ4に対応したプラグインのバージョンがどれになるのかをプラグインごとに確認をして、そのバージョンを探しだすのがパズルのような作業でした。ChatGPTのこともありますし、Webに情報を残すことで同様の問題や課題になっている人へ少しでもお役にたてばとおもい、ブログに残してみました。古いフレームワークなどをアップデートする作業はいつになっても無くならない作業なので、少しでも容易にできるようになると嬉しいのですが、AI技術の進化でそのあたりに関してもアプローチできていけそうですが、もう少し先になるような気が個人的にしております。

参考情報

お知らせ

アシアルでは、一緒に働くメンバーを募集しておりますので、気になる方はぜひ、採用ページを確認いただけますと幸いです。まずは、カジュアル面談からざっくばらんにお互いのことをお話できたらと思ってます。ご興味があるかたはこちらより、お問い合わせくださいませ。

author img for Hiroshi Sasagame
Hiroshi Sasagame

2002年からエンジニアとしてキャリアをスタートし、アシアルへは2007に中途入社。3年程度、エンジニアとしてPHPをメインにシステム構築に携わり、2010年以降では、プロジェクトマネージャとエンジニアを兼務で、いわゆる「フルスタックエンジニア」として業務を実施。 最近は、プロジェクトマネージャー・メンバーのアサイン管理・プロジェクトの全体を見るような統括業務をしながら、保守業務などでもコーディングする日々。

記事一覧

前の記事へ

次の記事へ

一覧へ戻る

「ノウハウ & Tips」カテゴリの最新記事

PAGE TOP