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に移行する際の関連するビルド実施時に関するパッケージのバージョン情報などの変更点を調査し、洗い出す作業をおこないました。
パッケージ | webpack3 | webpack4 | 備考 |
webpack | 3.8.1 | 4.32.2 | webpackの本体のアップデート |
webpack-cli | - | 3.3.2 | |
webpack-bundle-analyzer | 2.9.1 | 3.3.2 | webpack4対応のものにアップデート |
webpack-dev-server | - | 3.4.1 | ローカルサーバ用ライブラリ webpack-dev-middlewareのバージョンアップにて 必要になったため追加 |
webpack-merge | 4.1.1 | 4.2.1 | webpack4対応のものにアップデート |
html-webpack-plugin | 2.30.1 | 3.2.0 | HTML書き出しライブラリ |
extract-text-webpack-plugin | 3.0.2 | - | webpack4にて仕様が非推奨になった |
mini-css-extract-plugin | - | 0.6.0 | extract-text-webpack-pluginのかわりのもの |
optimize-css-assets-webpack-plugin | 3.2.0 | 5.0.1 | webpack4対応のものにアップデート |
uglifyjs-webpack-plugin | - | 2.2.0 | webpack3のときは同梱されたいたが、 webpack4からはplugin化されて、 optiomization.minimizer配下に変更 |
上記にて洗い出した情報をもとに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の内容を参考に以下の手順にて実施をおこないました。
- build/utils.jsのwebpack3からwebpack4へ対応するための修正を適応する
- mini-css-extract-pluginプラグインを読み込み設定と以前のextract-text-webpack-pluginプラグインを削除
- vue-style-loaderのからMiniCssExtraPluginのloaderに変更
- build/webpack.prod.conf.jsのwebpack3からwebpack4へ対応するための修正を適応する
- mini-css-extract-pluginプラグインを読み込み設定と以前のextract-text-webpack-pluginプラグインを削除
- uglifyjs-webpack-pluginプラグインとvue-loaderプラグインの読み込み設定を追加
- utilsのstyleLodersのusePostCSSの設定を追加
- new UglifyJsPluginとnew OptimizeCSSPluginの部分をコメントアウトして、optimizationの中に設定の記載を追加
- HtmlWebpackPluginのchunksSortModeの設定をコメントアウト
- HtmlWebpackPluginにあるwebpack.optimize.CommonsChunkPluginの設定を削除
- VueLoaderPlugin、webpack.HashedModuleIdsPlugin、webpack.optimize.ModuleConcatenationPluginの生成を追加
- 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.」が表示されたときは苦労がかたちになったということでホッとしました。
まとめ
実際にwebpack3からwebpack4への対応をしていきましたが、webpackに関連したプラグインのバージョンアップや必要になるプラグインの追加、非対応になったプラグインの削除などについて、確認や判断するのがとても苦労しました。webpackがすでにバージョンが5となっていることもあり、すべて最新にしても動作しなく、ひとつずつ4に対応したプラグインのバージョンがどれになるのかをプラグインごとに確認をして、そのバージョンを探しだすのがパズルのような作業でした。ChatGPTのこともありますし、Webに情報を残すことで同様の問題や課題になっている人へ少しでもお役にたてばとおもい、ブログに残してみました。古いフレームワークなどをアップデートする作業はいつになっても無くならない作業なので、少しでも容易にできるようになると嬉しいのですが、AI技術の進化でそのあたりに関してもアプローチできていけそうですが、もう少し先になるような気が個人的にしております。
参考情報
- https://qiita.com/shimarin/items/17707fa575744ca0bd89
- https://github.com/vuejs-templates/webpack
- https://gitee.com/xuliangzhan_admin/vue-webpack4-template
お知らせ
アシアルでは、一緒に働くメンバーを募集しておりますので、気になる方はぜひ、採用ページを確認いただけますと幸いです。まずは、カジュアル面談からざっくばらんにお互いのことをお話できたらと思ってます。ご興味があるかたはこちらより、お問い合わせくださいませ。