老项目vue2+webpack3项目升级教程

245 阅读4分钟

去年接手了公司一个项目,前一段时间,收到产品反馈说线上打开菜单非常的慢,点了几个列表页,网站就卡死了,再点击就没有反应了。用的 vue2.6.10 + webpack3 + element-ui2.15.7 项目很大,业务代码中使用了很多体积比较大的库,还有封装了大量的组件。

本地构建花费了 5 分钟,打包完 dist 文件有 16M 这么大,首屏加载大概有 7-8s,打开项目线上地址,我进去一看点了几个页面,果然出现产品和用户反馈的一样,卡顿明显,没几分钟,整个网站卡住了,刷新也还是卡。

那就开始整吧。

  • 如何排查问题

    • 使用 chrome 的开发者工具(打开网站,按 F12)

      如何使用可看官网文档,这里不赘述了

      developer.chrome.com/docs/devtoo…

      • Performance
      • Lighthouse
      • Network 勾选 Preverse Log 保留日志,勾选 Disable Cache 屏蔽浏览器的接口缓存机制,No throtting 选择器 slow3G 可以对当前网络状态进行检测,查看接口的响应体积和顺序
    • npm run preview -- --report 来分析 webpack 打包之后的各个静态资源的大小。你可以发现占用空间最多的是第三方依赖,前提是安装了包 webpack-bundle-analyzer

  • 接口慢
    因为这部分需要后端同事协助,所以在我发现存在一部分接口没有分页,前端数据量很大,记录下接口地址,然后开会和后端负责人讨论改造工作。(工作需要及时的安排和协调,让前后端同事工作并行,效率会比较高。
    经接口调整后,接口整个速度都更上去了,页面卡顿有所缓解

  • 减少 HTTP 请求

  • 升级 webpack 之前的项目结构 image.png

    yarn upgrade webpack@5.37.0
    yarn add webpack-dev-server webpack-cli -D
    npm-check-updates 一键升级所需的组件
    

    由于各种考虑 vue 和 element-ui 没有升级

    // package.json
    
    "scripts": {
        - "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
        + "dev": "npx webpack serve --config build/webpack.dev.conf.js --color --progress",
        "start": "npm run dev",
        "build": "node --max_old_space_size=2048 build/build.js"
    },
    
    

    webpack.base.conf.js

    - const merge = require('webpack-merge')
    + const { merge } = require('webpack-merge')
    

    新增 mode 选项

    module.exports = {
        + mode: process.env.NODE_ENV,
    }
    
    + const VueLoaderPlugin = require('vue-loader/lib/plugin');
    
    ...
     cacheGroups 对 chunks 的拆分起着关键的作用。可以通过 cacheGroups 来定制 chunks 拆分策略, 由于我们项目中存在很多插件,我们这里都把它单独拆分出来,配合cdn使用
    
    optimization: {
        splitChunks: {
        chunks: 'all',
        cacheGroups: {
            elementUI: {
                name: "chunk-elementUI",
                priority: 20,
                test: /[\\/]node_modules[\\/]element-ui[\\/]/
            },
            wangeditor: {
                name: "chunk-wangeditor",
                priority: 21,
                test: /[\\/]node_modules[\\/]wangeditor[\\/]/,
            },
            vue: {
                name: "chunk-vue",
                priority: 20,
                test: /[\\/]node_modules[\\/]vue[\\/]/
            },
            moment: {
                name: "chunk-moment",
                priority: 15,
                test: /[\\/]node_modules[\\/]moment[\\/]/,
            },
            lodash: {
                name: "chunk-lodash",
                priority: 15,
                test: /[\\/]node_modules[\\/]lodash[\\/]/,
            },
            axios: {
                name: "chunk-axios",
                priority: 15,
                test: /[\\/]node_modules[\\/]axios[\\/]/,
            },
            idValidator: {
                name: "chunk-idValidator",
                priority: 15,
                test: /[\\/]node_modules[\\/]id-validator[\\/]/,
            },
            libs: {
                name: "chunk-libs",
                test: /[\\/]node_modules[\\/]/,
                minChunks: 4,
                priority: 10,
                reuseExistingChunk: true,
            },
            common: {
                name: "chunk-common",
                test: resolve("src/components"), // 可自定义拓展你的规则
                minChunks: 4, // 最小共用次数
                priority: 5,
                reuseExistingChunk: true
            }
        }
        },
        runtimeChunk: {
            name: 'runtime'
        }
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: vueLoaderConfig
            },
            {
                test: /\.js$/,
                loader: 'babel-loader',
                include: [resolve('src'), resolve('test')],
                exclude: /node_modules/
            },
            {
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                loader: 'url-loader',
                options: {
                    limit: 10000,
                    name: utils.assetsPath('img/[name].[hash:7].[ext]'),
                }
            },
            {
                test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
                loader: 'url-loader',
                options: {
                    limit: 10000,
                    name: utils.assetsPath('media/[name].[hash:7].[ext]')
                }
            },
            {
                test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
                loader: 'url-loader',
                options: {
                    limit: 10000000, // 注意到这里我设置的很大,因为字体文件太大,大概28k,也采取转换成base64,减少http请求
                    name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
                }
            }
        ]
    },
    plugins: [
       - new webpack.optimize.CommonsChunkPlugin('common.js'),
        + new VueLoaderPlugin(),
        -    new webpack.optimize.CommonsChunkPlugin('common.js'),
        -    new webpack.ProvidePlugin({
        -     jQuery: "jquery",
        -       jquery: "jquery",
        -       "window.jQuery":"jQuery",
        -     $: "jquery"
        -    }),
    ],
    + externals: require('./cdn').externals
    ...
    
    

    webpack.dev.conf.js

    新增 mode 选项

    module.exports = {
        + mode: 'development',  // 'production', 'development' or '无 (none)'
        + externals: require('./cdn').externals
    }
    

    webpack.prod.conf.js

    - const ExtractTextPlugin = require('extract-text-webpack-plugin')
    + const MiniCssExtractPlugin = require("mini-css-extract-plugin")
    - const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
    
     plugins: [
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    new webpack.DefinePlugin({
      'process.env': env
    }),
    // new UglifyJsPlugin({
    //   uglifyOptions: {
    //     compress: {
    //       warnings: false,
    //       drop_debugger: true,
    //       drop_console: true
    //     }
    //   },
    //   sourceMap: config.build.productionSourceMap,
    //   parallel: true
    // }),
    // extract css into its own file
    new MiniCssExtractPlugin({
      filename: utils.assetsPath('css/[name].[contenthash].css'),
      // Setting the following option to `false` will not extract CSS from codesplit chunks.
      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
      allChunks: true,
    }),
    
    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    new OptimizeCSSPlugin({
      cssProcessorOptions: config.build.productionSourceMap
        ? { safe: true, map: { inline: false } }
        : { 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,
      hash: version,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true,
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      cdn: require('./cdn').cdn,
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      chunksSortMode: 'dependency'
    }),
    // keep module.id stable when vendor modules does not change
    new webpack.HashedModuleIdsPlugin(),
    // enable scope hoisting
    new webpack.optimize.ModuleConcatenationPlugin(),
    // split vendor js into its own file
    // new webpack.optimize.CommonsChunkPlugin({
    //   name: 'vendor',
    //   minChunks (module) {
    //     // any required modules inside node_modules are extracted to vendor
    //     return (
    //       module.resource &&
    //       /\.js$/.test(module.resource) &&
    //       module.resource.indexOf(
    //         path.join(__dirname, '../node_modules')
    //       ) === 0
    //     )
    //   }
    // }),
    // // extract webpack runtime and module manifest to its own file in order to
    // // prevent vendor hash from being updated whenever app bundle is updated
    // new webpack.optimize.CommonsChunkPlugin({
    //   name: 'manifest',
    //   minChunks: Infinity
    // }),
    // // This instance extracts shared chunks from code splitted chunks and bundles them
    // // in a separate chunk, similar to the vendor chunk
    // // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
    // new webpack.optimize.CommonsChunkPlugin({
    //   name: 'app',
    //   async: 'vendor-async',
    //   children: true,
    //   minChunks: 3
    // }),
    
    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
    ]
    
    

    build/util.js

    const ExtractTextPlugin = require('extract-text-webpack-plugin')
    - const ExtractTextPlugin = require('extract-text-webpack-plugin')
    + const MiniCssExtractPlugin = require("mini-css-extract-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 [MiniCssExtractPlugin.loader].concat(loaders)
            // return ExtractTextPlugin.extract({
            //     use: loaders,
            //     publicPath: '../../',
            //     fallback: 'vue-style-loader'
            // })
        } else {
            return ['vue-style-loader'].concat(loaders)
        }
    }
    
    

    splitChunks分离代码后,过大的插件被提取出来
  • 合理使用缓存

    • 静态图片和字体尽量缓存

    • cdn
    const isProduction = process.env.NODE_ENV === 'production';
    
    module.exports = {
      cdn: {
        css: [
          'https://unpkg.com/element-ui@2.15.9/lib/theme-chalk/descriptions.css',
        ],
        js: [
          // "https://unpkg.com/vue@2.6.10/dist/vue.min.js",
          // "https://unpkg.com/element-ui@2.15.7/lib/index.js",
          'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
          'https://unpkg.com/wangeditor@4.7.11/dist/wangEditor.min.js',
          'https://unpkg.com/xlsx@0.15.6/dist/xlsx.full.min.js',
          'https://unpkg.com/echarts@4.9.0/dist/echarts.min.js',
          'https://map.qq.com/api/gljs?v=1.exp&key=BSVBZ-5XSCX-EMQ4D-TIK2X-SIE2T-E6FQ5',
          // 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js',
          // 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/locale/zh-cn.min.js'
        ],
      },
      externals: {
        // vue: "Vue",
        // "element-ui": "ElementUI",
        vuex: 'Vuex',
        wangeditor: 'wangEditor',
        XLSX: 'xlsx',
        echarts: 'echarts',
        TMap: 'TMap',
        // moment: "moment",
      },
    };
    

    找到 public/index.html。将 js 和 css 资源注入。

    <head>
        <title>${process.env.APP_ENV_NAME}</title>
        <!-- 引入样式 -->
        <% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
            <link rel="stylesheet" href="<%=css%>">
        <% } %>
    </head>
    
    <body>
        <% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
            <script src="<%=js%>"></script>
        <% } %>
    
        <!-- built files will be auto injected -->
        <div id="app"></div>
    </body>
    
    
  • 文件压缩 - 图片压缩

    免费的web端工具[TinyPNG](https://tinify.cn/)将images拖进去,替换掉你的图片,尺寸大幅度压缩并保证质量。
    ![](https://s6.jpg.cm/2022/09/05/PA0KQi.png)
    - gzip压缩
    
    ![](https://s6.jpg.cm/2022/09/05/PA0hYk.png)
    
    ```js
    if (config.build.productionGzip) {
        const CompressionWebpackPlugin = require('compression-webpack-plugin')
    
        webpackConfig.plugins.push(
            new CompressionWebpackPlugin({
                asset: '[path].gz[query]',
                algorithm: 'gzip',
                test: new RegExp(
                    '\\.(' +
                    config.build.productionGzipExtensions.join('|') +
                    ')$'
                ),
                threshold: 100,
                minRatio: 0.8
            })
        )
    }
    ```
    

    折腾到这里,构建速度 40 左右,dist 压缩到 4M。首屏加载控制在 1s。效果显著!

  • 组件升级,支持虚拟加载 老项目中,使用 el-select,el-table 都不支持大数据虚拟加载,低版本存在卡顿现象 封装列表组件比较费时费力,版本 element-ui plus 支持虚拟加载。目前仅 keep-alive 来降低 dom 渲染消耗


原文:xuxia2013.github.io/2022/08/31/…

欣赏此文?求鼓励,求支持!