Webpack3升级到Webpack5

项目是公司的老项目,用的vue2.5 + webpack3,项目很大又复杂,因此构建速度太慢了,很多plugin也已经过时,低版本的劣势越来越明显。很多新特性和新插件只能在webpack5身上才能展现实力,那么开始改造吧。

首先就是原来的项目结构,主要在于build目录下面,存在基础配置,开发配置,生产配置
build
|-------util.js
|-------webpack.base.js
|-------webpack.dev.conf.js
|-------webpack.prod.conf.js
首先当然是把旧的依赖从package中升级了,这里直接先
npm uninstall webpack webpack-dev-server webpack-cli
然后安装新版本的webpack webpack-dev-server webpack-cli
npm install webpack@5.51.1
npm install -D webpack-dev-server@4.0.0 webpack-cli@4.8.0

主要是webpack,其他的依赖可以通过一个插件来一次性升级
npm install -g npm-check-updates
然后直接输入
npm-check-updates
即可等待全部版本包更新完毕,当然会有一些坑之后填就好
接下来就是package.json的启动方式,由于webpack-dev-server升到了4版本用来配套webpack5,启动脚本需要改写一下 
从webpack-dev-server 改成了 webpack serve 启动
"dev": "cross-env PROXY=true webpack serve --mode development --port 80 --progress --host 0.0.0.0 --config  build/webpack.dev.conf.js",
接下来就是对配置文件的修改了
 // Vue版本升级到了2.6+  (版本列表见文章末尾) 对应的template-complier也升级一致。这里注意,babel系列的插件版本依赖很强,比如babel-loader如果是7,babel-core就是6,如果babel-loader是8,babel-core就是7....还有其他的babel系列插件也需要统一调整,但只要小版本号变动即可,统一升级了小版本号,大版本号仍然沿用之前的。


// 首先是webpack.base.conf.js
引入了
const { VueLoaderPlugin } = require('vue-loader') 插件用来解析vue文件
{
test: /.vue$/,
loader: 'vue-loader'
},
在plugin中加入new VueLoaderPlugin(),

// 对于loader的配置,webpack5自带了资源解析,所以不需要什么url-loader,file-loader之类的,直接用assets就可以解析,如下图展示了新属性的替代性
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const { VueLoaderPlugin } = require('vue-loader')  // vue版本2.6.14

function resolve (dir) {
  return path.join(__dirname, '..', dir)
}
...

module.exports = {
  context: path.resolve(__dirname, '../'),
  entry: {
    ...
  },
  output: {
    ...
  },
  resolve: {
    ...
  },
  module: {
    rules: [
      ...(config.dev.useEslint ? [createLintingRule()] : []),
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      ...
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024,
          },
        },
        generator: {
          filename: utils.assetsPath('img/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024,
          },
        },
        generator: {
          filename: utils.assetsPath('media/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024,
          },
        },
        generator: {
          filename: utils.assetsPath('/fonts/[name].[hash:7].[ext]')
        }
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    ...
  ],
  ...
}
// webpack.dev.conf.js
首先webpack-merge要结构出来,这个和之前有不同
const { merge } = require('webpack-merge')

改动点:
// new webpack.NamedModulesPlugin(), // webpack5改用配置方式 插件作用: 在热加载时直接返回更新文件名
新:
optimization: {
moduleIds: 'named' // webpack5 采用此方式代替 NamedModulesPlugin
}

改动点:
// 旧copyWebpackPlugin插件使用方式
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
]),
新:
new CopyWebpackPlugin({
patterns:[{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
globOptions: { // webpack5 ignore要写在globOptions这里
ignore: ['.*']
}
}]
}),

改动点:

webpack-dev-server 这个改动的比较多,因为v3升级到v4有很多属性的调整,见https://github.com/webpack/webpack-dev-server/blob/master/migration-v4.md

devServer: {
    // clientLogLevel: 'warning', // v4 移动到了client下面 改名logging 'warn'
    client: {
      logging: 'warn',
      overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,
      progress: true,
    },
    static: false,
    devMiddleware: {
      publicPath: config.dev.assetsPublicPath
    },
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    hot: true,
    // contentBase: false,   // v4移动到了static下面
    compress: true,
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,
    // overlay: config.dev.errorOverlay
    //   ? { warnings: false, errors: true }   // v4 移动到了client下面
    //   : false,
    // publicPath: config.dev.assetsPublicPath, // v4 移动到了devMiddleware下面
    proxy: process.env.PROXY === 'true' ? config.dev.proxyTable : {},
    // quiet: true, // v4移除了该选项
    // watchOptions: {
    //   poll: config.dev.poll,   // v4移动到了static下面的watch
    // },
    // disableHostCheck: true // v4移除了该选项
    allowedHosts: "all"
  },
target: 'web'
// proxyTable里:
'/api': {
target: url,
secure: false,
changeOrigin: true,
onProxyReq: function (proxyReq, req, res) {
if (/^(localhost|[1-9][0-9]+)/.test(req.headers.host)) {
//写死cookie
proxyReq.setHeader(
'Cookie',
'xxxxxxxx'
);
} else {
req.headers && req.headers.cookie && proxyReq.setHeader('Cookie', req.headers.cookie);
}
}
},
// 生产环境
// 需要注意,一些旧的插件由于webpack新版已经不再支持,所以如果保留会报错,主要就是extract-text-webpack-plugin和optimize-css-assets-webpack-plugin。
分别用mini-css-extract-plugin和css-minimizer-webpack-plugin来代替
// const ExtractTextPlugin = require('extract-text-webpack-plugin') // 过期
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
// const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') // 压缩css过时,webpack5不用
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')  // webpack5使用
// 使用
plugins: [
   ...
    // new UglifyJsPlugin({   // 旧版写法
    //   uglifyOptions: {
    //     compress: {
    //       warnings: false
    //     }
    //   },
    //   sourceMap: config.build.productionSourceMap,
    //   parallel: true
    // }),

   new MiniCssExtractPlugin(),

   // new webpack.HashedModuleIdsPlugin(), // w5弃用  改用moduleIds: 'hashed',
   // new ExtractTextPlugin({   // 过期插件,弃用
    //   filename: utils.assetsPath('css/[name].[contenthash].css'),
    //   allChunks: true,
    // }),
    // new OptimizeCSSPlugin({   // 过时插件,弃用
    //   cssProcessorOptions: config.build.productionSourceMap
    //     ? { safe: true, map: { inline: false } }
    //     : { safe: true }
    // }),
   ...
],
optimization: {
    moduleIds: 'hashed',
    minimizer: [ 
      new CssMinimizerWebpackPlugin(), 
      new UglifyJsPlugin({         // 新版的丑化代码插件写在minimizer里
        uglifyOptions: {
          compress: {}
        },
        chunkFilter: (chunk) => chunk.name !== 'vendors',
        sourceMap: config.build.productionSourceMap,
        parallel: true,
      }),
    ],
}
代码拆分,之前采用new webpack.optimize.CommonsChunkPlugin拆分,现在webpack5能更智能拆分代码,通过配置optimization.splitChunks属性来实现功能,同时也可以拆分样式
splitChunks: {
      chunks: 'all',
      minSize: 30000,
      minChunks: 1,
      automaticNameDelimiter: '~',
      cacheGroups: {
        vendors: {
          name: 'vendors',
          test({ resource }) {
            return /[\\/]node_modules[\\/]/.test(resource);
          },
          priority: 10,
        },
        styles: {
          name: "styles",
          test: /\.(le|c)ss$/,
          type: "css/mini-extract",
          chunks: "all",
          enforce: true,
        },
      },
    },
// 注意HtmlWebpackPlugin中的chunksSortMode现在只有'none' | 'auto' | 'manual'三种配置,目前配置成auto即可
通过配置cache属性(和plugins并列),能够实现惊人的缓存效果,性能提升爆炸。
增量编译(官方称作:优化持久化缓存) ,

Webpack5之前在构建时,会以配置的 entry 为入口,递归解析模块依赖,构建出一个依赖图(graph),该依赖图记录代码中各个 module 之间的关系。
 每当有文件内容更新的时候,会重新递归生成依赖图,如果简单粗暴地重建依赖图再编译,会有很大的性能开销。在webpack5中,利用缓存实现增量编译,从而提升构建性能。每当代码变化、模块之间依赖关系改变导致依赖图改变时, Webpack 会读取记录做增量编译。 
cache: {
    type: "filesystem",
    buildDependencies: {
      config: [ __filename ]
    },
    cacheDirectory: path.resolve(__dirname, '../temp_cache'),
    name: 'scf-cache',   // 路径temp_cache/scf-cache   
    compression: 'gzip',
    idleTimeoutAfterLargeChanges: 1000,
    maxAge: 5184000000   // 未使用的缓存留在文件系统缓存中的时间 默认存在1个月
  }
实测了一下效果,首次构建与修改代码后第二次构建时长如下:
首次编译时间145.8s
修改某处代码之后二次编译时间49s
这还是因为生产环境开启了代码丑化UglifyJsPlugin限制了依赖检索构建的性能,如果不使用代码丑化插件直接采用增量编译,性能提升更明显
不开启Uglify丑化,第一次全量编译96.8s
不开启Uglify丑化,修改某处代码后二次编译时间13.9s
不开启Uglify丑化,不做任何修改,编译时间仅使用6.2s
其实Webpack5自带了开箱即用的代码压缩插件  
terser-webpack-plugin
只需要这个插件便不需要UglifyJsPlugin了,配置一下
const TerserPlugin = require("terser-webpack-plugin")   // webpack5开箱自带

optimization: {
    ...
    usedExports: true,  // 去查看哪些导出的模块被使用,然后再进行打包
    minimize: true,  // 压缩
    minimizer: [
      new TerserPlugin(),   // 使用
      new CssMinimizerWebpackPlugin(), 
      // new UglifyJsPlugin({   // 不再需要
      //   uglifyOptions: {
      //     compress: {}
      //   },
      //   chunkFilter: (chunk) => chunk.name !== 'vendors',
      //   sourceMap: config.build.productionSourceMap,
      //   parallel: true,
      // }),
    ],
}
使用Terser,修改一个地方编译时间23.2s
使用Terser,不做任何修改编译时间6.75s
性能提升效果明显,极限对比:全量编译145秒,而极限增量仅需要不到7秒!
util.js文件中修改了一处 MiniCssExtractPlugin.loader
const MiniCssExtractPlugin = require("mini-css-extract-plugin")

function generateLoaders(loader, loaderOptions) {
    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
    if(loader === 'less') loaders.push(lessResource)
    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 ExtractTextPlugin.extract({
      //   use: loaders,
      //   fallback: 'vue-style-loader'
      // })
      return [{loader: MiniCssExtractPlugin.loader}].concat(loaders)
    } else {
      return ['vue-style-loader'].concat(loaders)
    }
  }
对于sourceMap也有变化,之前的命名规范不再适用

// 新命名格式 ^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$
// devtool: 'cheap-module-eval-source-map', 
    devtool: 'eval-cheap-module-source-map',   // webpack5 改为 eval-cheap-module-source-map




Webpack5好处多多,个人最在意的还是模块联邦特性,放到之后来说

参考知乎:https://zhuanlan.zhihu.com/p/348612482

Webpack5官方文档:
https://webpack.docschina.org/concepts/
Webpack5最全配置范例:
https://github.com/webpack/webpack/blob/main/schemas/WebpackOptions.json
package包版本如下,供参考:
"dependencies": {
    "@babel/polyfill": "^7.7.0",
    "@sentry/vue": "6.11.0",
    "@sentry/webpack-plugin": "^1.17.1",
    "animate.css": "^4.1.1",
    "bignumber.js": "^9.0.1",
    "clipboard": "^2.0.8",
    "cross-env": "^7.0.3",
    "css-loader": "^6.2.0",
    "element-ui": "2.15.5",
    "html2canvas": "^1.3.2",
    "jspdf": "2.3.1",
    "lodash": "^4.17.21",
    "moment": "^2.29.1",
    "node-sass": "^6.0.1",
    "querystring": "^0.2.0",
    "resize-detector": "^0.3.0",
    "sass-loader": "^12.1.0",
    "vant": "^2.12.26",
    "vue": "^2.6.14",
    "vue-awesome-swiper": "^4.1.1",
    "vue-cookies": "^1.7.4",
    "vue-echarts": "^6.0.0",
    "vue-loader": "^15.9.8",
    "vue-router": "3.5.2",
    "vue-seamless-scroll": "^1.1.23",
    "vue-style-loader": "^4.1.3",
    "vuejs-dialog": "^1.4.2",
    "vuex": "^3.6.2",
    "webpack-theme-color-replacer": "^1.3.26"
  },
  "devDependencies": {
    "@babel/plugin-syntax-top-level-await": "^7.14.5",
    "@commitlint/cli": "^13.1.0",
    "@commitlint/config-conventional": "^13.1.0",
    "autoprefixer": "^10.3.2",
    "babel-core": "^6.22.1",
    "babel-eslint": "^8.2.1",
    "babel-helper-vue-jsx-merge-props": "^2.0.3",
    "babel-loader": "^7.1.1",
    "babel-plugin-component": "^1.1.1",
    "babel-plugin-import": "^1.13.3",
    "babel-plugin-syntax-jsx": "^6.18.0",
    "babel-plugin-transform-decorators-legacy": "^1.3.5",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-plugin-transform-vue-jsx": "^3.7.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-stage-2": "^6.24.1",
    "babel-register": "^6.26.0",
    "chalk": "^4.1.2",
    "copy-webpack-plugin": "^9.0.1",
    "cross-spawn": "^7.0.3",
    "css-loader": "^6.2.0",
    "css-minimizer-webpack-plugin": "^3.0.2",
    "eslint": "^7.32.0",
    "eslint-config-airbnb-base": "^14.2.1",
    "eslint-config-prettier": "^8.3.0",
    "eslint-config-standard": "^16.0.3",
    "eslint-friendly-formatter": "^4.0.1",
    "eslint-loader": "^1.7.1",
    "eslint-plugin-import": "^2.24.2",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-prettier": "^3.4.1",
    "eslint-plugin-promise": "^5.1.0",
    "eslint-plugin-standard": "^3.0.1",
    "eslint-plugin-vue": "^7.16.0",
    "friendly-errors-webpack-plugin": "^1.7.0",
    "html-webpack-plugin": "^5.3.2",
    "husky": "7.0.2",
    "less": "^4.1.1",
    "less-loader": "^4.1.0",
    "lint-staged": "^11.1.2",
    "mini-css-extract-plugin": "^2.2.0",
    "nightwatch": "^1.7.8",
    "node-less": "^1.0.0",
    "node-notifier": "^10.0.0",
    "ora": "^6.0.0",
    "portfinder": "^1.0.28",
    "postcss-import": "^14.0.2",
    "postcss-loader": "^6.1.1",
    "postcss-url": "^10.1.3",
    "prettier": "^2.3.2",
    "rimraf": "^3.0.2",
    "style-loader": "^3.2.1",
    "style-resources-loader": "^1.4.1",
    "uglifyjs-webpack-plugin": "^2.2.0",
    "vue-template-compiler": "^2.6.14",
    "webpack": "^5.51.1",
    "webpack-bundle-analyzer": "^4.4.2",
    "webpack-cli": "^4.8.0",
    "webpack-dev-server": "^4.0.0",
    "webpack-merge": "^5.8.0",
    "webpack-theme-color-replacer": "^1.3.26"
  },