背景
项目是基于vue2和webpack3的项目,然后这个项目经过逐年的迭代,代码和文件数量都非常的庞大,开发运行和编译运行的速度都非常的慢,有时候你去吃完饭回来还在运行。

开始探索
最开始简单的认为只要设置webpack运用多线程、babel开启缓存等在网上能查到的优化方式就应该能解决这个问题,但是这一套的东西整下来发现效果并不明显,打包时间依然是很慢,还是二十多分钟,也就是说这些方式已经没办法解决这个祖传项目了。那么我们就去尝试升级一下我们的webpack,试一下能不能解决这个问题。
vue2 webpack3升级webpack5全过程
首先确认一点,webpack5需要node10.13.0以上的版本,也不建议升级成最新的版本,我测试过14和16的版本,都是可以支持升级后的运行和打包的。
首先贴一下package文件
perl
{
"version": "1.0.0",
"description": "A Vue. js project",
"author": "",
"private": true,
"scripts": {
"dev": "cross-env NODE_ENV=development webpack server --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"unit": "jest --config test/unit/jest.conf.js --coverage",
"eZe": "node test/e2e/runner.js",
"test": "npm run unit && npm run ele",
"lint": "eslint --ext js, vue sc test/unit test/e2e/specs",
"build:dev": "cross-env NODE_ENV=development ENV_CONFIG=dev node build/build.js",
"build:sit": "cross-env NODE_ENV=sit ENV_CONFIG=sit node build/build.js",
"build:uat": "cross-env NODE_ENV=uat ENV CONFIG=uat node build/build.js",
"build:prod": "cross-env NODE_ENV=production ENV_CONFIG=prod node build/build. js"
},
"dependencies": {
"@babel/runtime": "^7.27.6",
"@babel/runtime-corejs2": "^7.27.6",
"axios": "^0.19.0",
"babel-loader": "18.0.0",
"echarts": "^5.4.2",
"element-ui": "^2.13.2",
"jquery": "^3.7.0",
"svg-sprite-loader": "A6.0.11",
"vue": "^2.5.2",
"vue-audio-visual": "A2.4.1",
"vue-drag-resize": "^1.3.2",
"vue-lazy-component": "A0.1.5",
"vue-router": "13.3.4",
"vue-socket.io": "^3.0.9",
"vuedraggable": "^2.23.0",
"vuex": "^3.6.2",
"webpack-jquery-ui": "^2.0.1",
"webpackbar": "^5.0.0-3"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/plugin-proposal-decorators": "^7.0.0",
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
"@baba/plugin-proposal-function-sent": "^7.0.0",
"@baba/plugin-proposal-json-strings": "^7.0.0",
"@babel/plugin-proposal-numeric-separator": "^7.0.0",
"@babel/plugin-proposal-throw-expressions": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/plugin-syntax-jsx": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@soda/friendly-errors-webpack-plugin": "^1.8.1",
"@swc/core": "^1.12.7",
"autoprefixer": "^7.1.2",
"babel-eslint": "19.0.0",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-jest": "^21.0.2",
"babel-plugin-dynamic-import-node": "^1.2.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-register": "^6.22.0",
"chalk": "^2.0.1",
"copy-webpack-plugin": "^4.0.1",
"cross-env": "^7.0.0",
"cross-spawn": "15.0.1",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.2",
"eslint": "^4.15.0",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.0.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.4.0",
"jest": "^22.0.4",
"jest-serializer-vue": "^0.3.0",
"mini-css-extract-plugin": "^2.4.3",
"nightwatch": "^0.9.12",
"node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"path-browserify": "^1.0.1",
"portfinder": "^1.0.13",
"postcss-import": "^11.0.0",
"postcss-loader": "^8.1.1",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
"sass": "٨1.30.0",
"sass-loader": "^10.1.0",
"selenium-server": "13.0.1",
"semver": "^5.3.0",
"shelljs": "10.7.6",
"terser-webpack-plugin": "^5.3.14",
"url-loader": "^4.1.1",
"vue-jest": "^1.0.2",
"vue-loader": "^15.8.0",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.6.14",
"webpack": "5.37.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.7.4",
"webpack-merge": "^5.7.3",
"webpack-parallel-uglify-plugin": "^1.1.2"
},
"engines": {
"node": ">=6.0.0",
"npm": ">=3.0.0"
},
"browserslist": [
">1%",
"last 2 versions",
"not ie <= 8"
]
}
升级过程中遇到的问题
1. 打包后发现图片、字体图标加在失败,而且图片和字体图标的输出目录在根目录下,没有输出在对应的static下的img和fonts目录下
经过排查和阅读webpack的文档发现,老项目用的url-loader、file-loader等配置会与webpack5本身的asset模块冲突,所有升级后的项目需要使用webpack的asset、asset/resource和asset/inline去处理了。
2. 打包后部署发现字体图标加载失败,并显示404
经过排查发现是MiniCssExtractPlugin.loader抽取css文件时,publicPath设置的不对,导致在查找字体图标时的路径不对,所以在配置文件中需要设置"../../"。
3. 运行npm run dev 页面显示GET /404
经过排查发现升级webpack5以后,publicPath需要区分development和production,分别设置为"/"和"./"。
4. 运行成功后发现无法主动跳转到首页
经过排查和查阅文档发现,webpack5不在主动去处理node的path模块的浏览器兼容问题,页面代码用到了这个path模块,所以导致无法正确加载,解放方式使用path-browserify替换node的path模块。
5. 不支持>>> 这种样的穿透语法
修改页面代码使用::deep或者:deep()替换老的语法。
6. 在开发环境下页面报错会有overlay阻塞调试
在webpack的配置文件中添加client:{overlay:false}
配置文件
Utils.js
javascript
'use strict'
const path = require('path')
const config = require('../config')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const packageConfig = require('../package.json')
exports.assetsPath = function (_path) {
const assetsSubDirectory = config.build.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders(loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader.postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../../',
}
}
].concat(loaders)
} else {
return ['vue-style-loader'].concat(loaders)
}
}
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass'),
scss: generateLoaders('scss'),
}
}
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\.' + extension, 'g'),
use: loader,
})
}
return output
}
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ':' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png'),
})
}
}
webpack.base.conf.js
javascript
'use strict'
const webpack = require('webpack')
const path = require('path')
const utils = require(' /utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
const {VueLoaderPlugin} = require("vue-loader")
//const HappyPack = require( 'happypack' )
function resolve(dir) {
return path.join(__dirname, "..", dir)
}
const createLintingRule = () => ({
test: /١. (js| vue) $/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsIn0verlay
}
})
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: config.build.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json', 'scss'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
'img': resolve('src/assets')
},
modules: [
resolve('src'),
resolve('node modules')
]
},
module: {
rules: [
{
test: /.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /.js$/,
use:['babel-loader'],
include: [resolve('src'), resolve('test')],
exclude: /node_modules/
},
{
test: /.(png|jpg|jpeg|gif|svg)(?.*)?$/,
type: 'asset/resource',
exclude: /node_modules/,
generator:{
filename: 'img/[name].[hash].[ext]',
}
},
{
test: /.(mp4|webm|ogg|mp3|wav|m4a|aac|oga|flac)(?.*)?$/,
type: 'asset/resource',
exclude: /node_modules/,
generator:{
filename: 'media/[name].[hash].[ext]',
}
},
{
test: /.(woff|woff2|eot|ttf|otf)(?.*)?$/,
type: 'asset/resource',
generator:{
filename: 'fonts/[name].[hash].[ext]',
}
}
]
},
node: {
global: true
},
plugins: [
new webpack.ProvidePlugin({
JQuery: "jquery",
$: "jquery"
}),
new VueLoaderPlugin()
]
}
webpack.dev.conf.js
php
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const {merge} = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require(' copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('@soda/friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({sourceMap: config.dev.cssSourceMap, usePostCSS: true})
},
// these devServer options should be customized in /config/index.js
devServer: {
// clientLogLevel: 'warning'.
client: {
overlay: false
},
historyApiFallback: {
rewrites: [
{from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html')}
]
},
hot: true,
compress: true,
host: HOST || config.dev.host,
port: PORT || config.dev.port,
open: config.dev.auto0penBrowser,
proxy: config.dev.proxyTable,
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
}),
new CopyWebpackPlugin([{
from: path.resolve(__dirname, 'index.html'),
to: config.dev.assetsPublicPath,
ignore: ['.*']
}])
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
process.env.PORT = port
devWebpackConfig.devServer.port = port
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
message: [`xxxxxxxxxx`],
onError: err => reject(err)
},
}))
resolve(devWebpackConfig)
}
})
})
webpack.prod.conf.js
php
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require(' ../config')
const {merge} = require('webpack-merge')
const baseWebpackConfig = require(' ./webpack.base.conf')
const CopyWebpackPlugin = require(' copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const WebpackBar = require('webpackbar')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
const Version = new Date().getTime()
const env = config.build[process.env.ENV_CONFIG + 'Env']
const webpackConfig = merge(webpackConfig, {
mode: 'production',
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true,
})
},
output: {
path: config.build.outputPath,
filename: 'js/[name].[chunkhash].' + Version + '.js',
chunkFilename: 'js/[name].[chunkhash].' + Version + '.js',
},
optimization: {
minimize: true,
minimizer: [
new TerserWebpackPlugin({
minify: TerserWebpackPlugin.swcMinify,
parallel: true,
extractComments: false,
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
format: {
comments: false,
}
}
}),
new CssMinimizerPlugin({
minimizerOptions: {
presets: [
'default',
{
discardUnused: false,
normalizeUnicode: false,
reduceVariables: false,
}
]
}
})
],
runtimeChunk: {name: 'runtimeChunk'},
concatenateModules: true,
splitChunks: {
chunks: 'all',
cacheGroups: {
libs:{
name: 'chunk-libs',
test: /[\/]node_modules[\/]/,
priority: 10,
chunks: 'initial',
},
vue:{
name: 'vue',
test: /[\/]node_modules[\/]_?vue(.*)/,
priority: 20,
chunks: 'initial',
},
elementUI:{
name: 'element-ui',
priority: 0,
test: /[\/]node_modules[\/]_?element-ui(.*)/
},
echarts:{
name: 'echarts',
priority: 20,
test: /[\/]node_modules[\/]_?echarts(.*)/
},
jquery:{
name: 'jquery',
priority: 20,
test: /[\/]node_modules[\/]_?jquery(.*)/
},
vuedraggable:{
name: 'vuedraggable',
priority: 20,
test: /[\/]node_modules[\/]_?vuedraggable(.*)/
},
fortawesome:{
name: 'fortawesome',
priority: 20,
test: /[\/]node_modules[\/]_?fortawesome(.*)/
}
}
},
moduleIds: 'deterministic'
},
plugins: [
new webpack.DefinePlugin({
'process.env': env
}),
new MiniCssExtractPlugin({
filename: 'css/[name].css',
chunkFilename: 'css/[name].css',
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: 'true',
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
},
chunksSortMode: 'auto'
}),
new CopyWebpackPlugin([{
from: path.resolve(__dirname, './static'),
to: config.build.outputPath,
ignore: ['.*'],
}]),
new WebpackBar(),
]
})
if (config.build.productionGzip){
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(new CompressionWebpackPlugin({
asset: '[path][name].gz]',
algorithm: 'gzip',
test: /.js$/,
threshold: 10240,
minRatio: 0.8,
}))
}
module.exports = webpackConfig
总结
升级完webpack5以后,对应的项目开发环境运行速度和编译速度都有质的提升,我们的项目测试下来,编译打包从之前的30多分钟到现在的6分钟,开发运行从之前的20多分钟到现在的2到3分钟,可以说升级webpack5后确实让我们快了很多。