config.js

const webpack = require('webpack')
const BundlePlugin = require('webpack-bundle-analyzer')
const CompressionPlugin = require('compression-webpack-plugin')

const envTime = new Date().getTime()
const envNode = process.env.VUE_APP_MODE
const product = Boolean(envNode === 'pro')

// CDN引入文件配置列表
const scriptlist = {
  css: ['https://cdn.jsdelivr.net/npm/vant@2.12.37/lib/index.min.css'],
  js: [
    'https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js',
    'https://cdn.jsdelivr.net/npm/vue-router@3.2.0/dist/vue-router.min.js',
    'https://cdn.jsdelivr.net/npm/vuex@3.4.0/dist/vuex.min.js',
    'https://cdn.jsdelivr.net/npm/axios@0.24.0/dist/axios.min.js',
    'https://cdn.jsdelivr.net/npm/vant@2.12.3/lib/vant.min.js',
    'https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.min.js',
    'https://cdn.jsdelivr.net/npm/echarts@5.2.2/dist/echarts.min.js',
    'https://cdn.jsdelivr.net/npm/html2canvas@1.3.3/dist/html2canvas.min.js',
    'https://cdn.jsdelivr.net/npm/signature_pad@3.0.0-beta.4/dist/signature_pad.umd.min.js',
    'https://cdn.jsdelivr.net/npm/tinymce@5.10.2/tinymce.min.js',
    'https://cdn.jsdelivr.net/npm/@packy-tang/vue-tinymce@1.1.2/dist/vue-tinymce.umd.js'
  ],
  sign: [
    'sha384-OZmxTjkv7EQo5XDMPAmIkkvywVeXw59YyYh6zq8UKfkbor13jS+5p8qMTBSA1q+F',
    'sha384-q2k+qGH0mwIAi2a3vbwlfY5KFOAQu6gKv1U2qcefUUVumCvayxYUx2NI125eTHkA',
    'sha384-mE6GdOegC4x8XBAer/qsG6HyKLvw9mOi3Kjh3Jy6VX9RcKnrRuS/fG2OkVYeh3Zu',
    'sha384-I2unte9Y12QMna3v071F1Nka0u7+32War1ddTq53DRJ7CSNfyRctw9b7oHce7hey',
    'sha384-KjscdaP49SEKsCc9BDAeJETME8lF6l5b9Ia4Qa9W7FyHDgAgKJjknVRS4Hfjawbk',
    'sha384-EgMpPgA8t6YITzfRZLLpZrVASrd7xd2dHvJCUYTH6YeRSNdorHBmyA5GRmJ328xc',
    'sha384-zEboaczr+mRTdFbfynqn3FUVKNSHEcnpUUudSYLptsxK3zgb1UvCGfgEA2g33Dxe',
    'sha384-SDHfRgULmZeD9p9uDA58EAdSvla8KNTW+tqSkvy6S0pYp2dmLqUJBAx3SZl8cq7f',
    'sha384-Ls3uq+Y1QgaClb9daHoPrarDb+ZlWOGUnH1svrxe5OdEYzhNTdqEoLGRpSCYGQXm',
    'sha384-FeTeQyBGB7Ipedniv1qfR8RtmJ1Yq6guxLPnRNucAA47AwrKMIuDPGjMQ0lLltrM',
    'sha384-zwJhfl6tJG492ETsDCCkzV0skjhExHFLznFSKCPJ/G02HOsIjd/2p3KAcRQsCl/a'
  ]
}

module.exports = {
  publicPath: '/gdyb-h5/',
  assetsDir: 'static',
  filenameHashing: false,
  productionSourceMap: product,
  css: {
    sourceMap: envNode === 'sit',
    loaderOptions: {
      sass: {
        additionalData: `
          @import "@/assets/scss/variable.scss";
          @import "@/assets/scss/mixin.scss";
        `
      }
    },
    extract: product ? { ignoreOrder: true } : false
  },
  pages: {
    index: {
      entry: './src/main.js',
      template: './public/index.html',
      filename: 'index.html',
      title: 'gdyb-h5',
      chunks: ['chunk-vendors', 'chunk-common', 'index'],
      cdn: scriptlist
    }
  },
  configureWebpack: (config) => {
    // 配置CDN,打包忽略文件
    config.externals = {
      vue: 'Vue',
      'vue-router': 'VueRouter',
      vuex: 'Vuex',
      axios: 'axios',
      vant: 'vant',
      moment: 'moment',
      echarts: 'echarts',
      html2canvas: 'html2canvas',
      signature_pad: 'SignaturePad',
      tinymce: 'tinymce',
      'vue-tinymce': 'VueTinymce'
    }

    // 文件添加时间戳
    config.output = Object.assign(config.output, {
      filename: `static/js/[name]-${envTime}.js`,
      chunkFilename: `static/js/[name]-${envTime}.js`
    })

    // 配置打包GZIP
    const compression = new CompressionPlugin({
      algorithm: 'gzip',
      filename: '[path].gz[query]',
      test: /\.js$|\.html$|\.css$/,
      minRatio: 1,
      threshold: 10240,
      deleteOriginalAssets: false
    })

    const limitchuck = new webpack.optimize.LimitChunkCountPlugin({
      maxChunks: 10,
      minChunkSize: 1000
    })

    // 打包分析
    const bundleAnalyzer = new (BundlePlugin.BundleAnalyzerPlugin)()

    config.plugins.push(compression, limitchuck, bundleAnalyzer)
    config.entry.app = ['babel-polyfill', './src/main.js']

    // 打包关闭console
    config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true
  },
  devServer: {
    host: 'localhost',
    port: 8080,
    proxy: process.env.VUE_APP_BASE_API
  }
}

index.html(src/public/index)

<!DOCTYPE html>
<html lang="">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="renderer" content="webkit">
    <meta name="referrer" content="never">
    <meta name="x5-orientation" content="portrait">
    <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0">
    <title>xxxxxxx</title>
    <% for (const i in htmlWebpackPlugin.options.cdn.css) { %>
        <link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
    <% } %>
</head>
<body>

<noscript>
    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
        Please enable it to continue.</strong>
    </noscript>

    <div id="app"></div>
    <!-- built files will be auto injected -->

    <% for (const i in htmlWebpackPlugin.options.cdn.js) { %>
        <script
            type="text/javascript"
            crossorigin="anonymous"
            src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"
            integrity="<%= htmlWebpackPlugin.options.cdn.sign[i] %>"
        ></script>
    <% } %>
</body>
</html>

报错信息汇总:


// compression-webpack-plugin版本太高,建议使用@5.0.1
ERROR TypeError:Cannot read property ‘tapPromise‘ of undefined


// 代码分析报告必须使用new BundleAnalyzerPlugin()
TypeError: Class constructor BundleAnalyzerPlugin cannot be invoked without 'new';


// 端口占用错误提示,杀掉占用端口即可
throw er; // Unhandled 'error' event
Error: listen EADDRINUSE: address already in use 127.0.0.1:8888


// 计算文件性能入口文件大小限制
The following entrypoint(s) combined asset size exceeds the recommended limit


// mini-css-extract-plugin,css引入顺序不一致
chunk chunk-common [mini-css-extract-plugin] Conflicting order.

推荐阅读相关[mini-css-extract-plugin]问题解析:
https://laysent.com/til/2019-11-28_conflicting-order-in-mini-css-extract-plugin

生成integrity SRI Hash链接工具网站: