阅读 6262

vuecli3+webpack4优化实践(删除console.log和配置dllPlugin)

本文主要介绍如何在vuecli3生成的项目中,打包输出时删除console.log和使用dllplugin,并记录了配置过程中踩到的坑。 (本人水平有限~希望大家多多指出有误的地方)

一、生产环境中删除console.log

在开发代码中写的console.log,可以通过配置webpack4中的terser-webpack-plugin插件达成目的,compress参数配置如下:

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
              warnings: false,
              drop_console: true,
              drop_debugger: true,
              pure_funcs: ['console.log']
          },
        },
      }),
    ],
  },
};
复制代码

而@vue/cli-service的配置源码也是使用了terser-webpack-plugin插件进行Tree Shaking,以下是@vue/cli-service/prod.js的源码

module.exports = (api, options) => {
  api.chainWebpack(webpackConfig => {
    if (process.env.NODE_ENV === 'production') {
      const isLegacyBundle = process.env.VUE_CLI_MODERN_MODE && !process.env.VUE_CLI_MODERN_BUILD
      const getAssetPath = require('../util/getAssetPath')
      const filename = getAssetPath(
        options,
        `js/[name]${isLegacyBundle ? `-legacy` : ``}${options.filenameHashing ? '.[contenthash:8]' : ''}.js`
      )

      webpackConfig
        .mode('production')
        .devtool(options.productionSourceMap ? 'source-map' : false)
        .output
          .filename(filename)
          .chunkFilename(filename)

      // keep module.id stable when vendor modules does not change
      webpackConfig
        .plugin('hash-module-ids')
          .use(require('webpack/lib/HashedModuleIdsPlugin'), [{
            hashDigest: 'hex'
          }])

      // disable optimization during tests to speed things up
      if (process.env.VUE_CLI_TEST) {
        webpackConfig.optimization.minimize(false)
      } else {
        const TerserPlugin = require('terser-webpack-plugin')
        const terserOptions = require('./terserOptions')
        webpackConfig.optimization.minimizer([
          new TerserPlugin(terserOptions(options))
        ])
      }
    }
  })
}
复制代码

在 vue.config.js 中的 configureWebpack 选项提供一个对象会被 webpack-merge 合并入最终的 webpack 配置,因此vue-cli3构建的项目中只需要修改terserOptions即可,vue.config.js配置如下:

module.exports = {
  publicPath: '/',
  outputDir: 'dist',
  devServer: {
    port: 8080,
    https: false,
    hotOnly: true,
    disableHostCheck: true,
    open: true,
  },
  productionSourceMap: false, // 生产打包时不输出map文件,增加打包速度
  configureWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      config.optimization.minimizer[0].options.terserOptions.compress.warnings = false
      config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true
      config.optimization.minimizer[0].options.terserOptions.compress.drop_debugger = true
      config.optimization.minimizer[0].options.terserOptions.compress.pure_funcs = ['console.log']
    }
  }
}
复制代码

配置完成后使用 vue inspect --mode=production > output.js 命令审查项目的 webpack 配置,optimization.minimizer的输出如下:

optimization: {
    minimizer: [
      {
        options: {
          test: /\.m?js(\?.*)?$/i,
          chunkFilter: () => true,
          warningsFilter: () => true,
          extractComments: false,
          sourceMap: false,
          cache: true,
          cacheKeys: defaultCacheKeys => defaultCacheKeys,
          parallel: true,
          include: undefined,
          exclude: undefined,
          minify: undefined,
          terserOptions: {
            output: {
              comments: /^\**!|@preserve|@license|@cc_on/i
            },
            compress: {
              arrows: false,
              collapse_vars: false,
              comparisons: false,
              computed_props: false,
              hoist_funs: false,
              hoist_props: false,
              hoist_vars: false,
              inline: false,
              loops: false,
              negate_iife: false,
              properties: false,
              reduce_funcs: false,
              reduce_vars: false,
              switches: false,
              toplevel: false,
              typeofs: false,
              booleans: true,
              if_return: true,
              sequences: true,
              unused: true,
              conditionals: true,
              dead_code: true,
              evaluate: true,
              warnings: false, 
              drop_console: true, 
              drop_debugger: true, 
              pure_funcs: [
                'console.log'
              ]
            },
            mangle: {
              safari10: true
            }
          }
        }
      }
    ],
}
复制代码

到此完成删除console.log的配置,接下来记录一下我踩到的坑~

坑1:在vue.config.js中直接使用terser-webpack-plugin后,通过vue inpect审查发现新增的compress参数并没有直接进入原有的terserOptions,而是minimizer数组新增了一个对象。这样导致vue-cli原有的terser-webpack-plugin配置失效。打包会以cache和parallel为false的配置下进行打包输出,打包速度变慢,因此后来采取直接修改terserOptions。

minimizer数组新增了一个对象:

options: {
  test: /\.m?js(\?.*)?$/i,
  chunkFilter: () => true,
  warningsFilter: () => true,
  extractComments: false,
  sourceMap: false,
  cache: false, 
  cacheKeys: defaultCacheKeys => defaultCacheKeys,
  parallel: false,
  include: undefined,
  exclude: undefined,
  minify: undefined,
  terserOptions: {
    output: {
      comments: /^\**!|@preserve|@license|@cc_on/i
    },
    compress: {
      warnings: false,
      drop_console: true,
      drop_debugger: true,
      pure_funcs: [
        'console.log'
      ]
    }
  }
}
复制代码

坑2(未解决):在给.eslintrc.js的rules配置了no-console的情况下,修改代码后的首次打包eslint-loader总会在终端上报 error: Unexpected console statement (no-console),虽然打包过程中报错,但是最终的输出代码是没有console.log的;(使用babel-plugin-transform-remove-console删除console.log也会出现这种情况)

查看@vue/cli-plugin-eslint的源码发现eslint的cache属性为true,所以再次打包就不会对未修改的文件进行检测。

Eslint Node.js API对cache参数解释如下:

cache - Operate only on changed files (default: false). Corresponds to --cache.

cli-plugin-eslint使用eslint-loader的关键代码如下:

api.chainWebpack(webpackConfig => {
      webpackConfig.resolveLoader.modules.prepend(path.join(__dirname, 'node_modules'))

      webpackConfig.module
        .rule('eslint')
          .pre()
          .exclude
            .add(/node_modules/)
            .add(require('path').dirname(require.resolve('@vue/cli-service')))
            .end()
          .test(/\.(vue|(j|t)sx?)$/)
          .use('eslint-loader')
            .loader('eslint-loader')
            .options({
              extensions,
              cache: true, 
              cacheIdentifier,
              emitWarning: options.lintOnSave !== 'error',
              emitError: options.lintOnSave === 'error',
              eslintPath: resolveModule('eslint', cwd) || require.resolve('eslint'),
              formatter:
                loadModule('eslint/lib/formatters/codeframe', cwd, true) ||
                require('eslint/lib/formatters/codeframe')
            })
    })
复制代码

如果要终端不输出eslint的错误,可以在vue.config.js配置lintOnSave: process.env.NODE_ENV !== 'production'生产环境构建时禁用,但是这样与在eslintrc.js的rules中配置'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off'的目的自相矛盾。

那么是否有办法让eslint-loader在terser-webpack-plugin或者babel-plugin-transform-remove-console之后进行检测呢?还是说配置了删除console.log就没必要配置'no-console'呢?希望有大神能回答我这个疑惑!

二、使用dllPlugin优化打包速度

网上已经有很多文章介绍dllPlugin的使用方法,这里就不介绍dllPlugin的详细配置说明了。本文只介绍一下针对vue-cli3项目使用webapck-chain方式的配置代码,所以就直接贴代码啦~

新增webpack.dll.config.js,代码如下:

const path = require('path')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const webpack = require('webpack')

module.exports = {
  mode: 'production',
  entry: {
    vendor: ['vue/dist/vue.runtime.esm.js', 'vuex', 'vue-router', 'element-ui'],
    util: ['lodash']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, 'dll'),
    library: 'dll_[name]'
  },
  plugins: [
    new CleanWebpackPlugin(), // clean-wepback-plugin目前已经更新到2.0.0,不需要传参数path
    new webpack.DllPlugin({
      name: 'dll_[name]',
      path: path.join(__dirname, 'dll', '[name].manifest.json'),
      context: __dirname
    })
  ]
}
复制代码

在vue.config.js添加DllReferencePlugin,最终代码如下:

const webpack = require('webpack')
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')
const path = require('path')

const dllReference = (config) => {
  config.plugin('vendorDll')
    .use(webpack.DllReferencePlugin, [{
      context: __dirname,
      manifest: require('./dll/vendor.manifest.json')
    }])

  config.plugin('utilDll')
    .use(webpack.DllReferencePlugin, [{
      context: __dirname,
      manifest: require('./dll/util.manifest.json')
    }])

  config.plugin('addAssetHtml')
    .use(AddAssetHtmlPlugin, [
      [
        {
          filepath: require.resolve(path.resolve(__dirname, 'dll/vendor.dll.js')),
          outputPath: 'dll',
          publicPath: '/dll'
        },
        {
          filepath: require.resolve(path.resolve(__dirname, 'dll/util.dll.js')),
          outputPath: 'dll',
          publicPath: '/dll'
        }
      ]
    ])
    .after('html')
}

module.exports = {
  publicPath: '/',
  outputDir: 'dist',
  devServer: {
    port: 8080,
    https: false,
    hotOnly: true,
    disableHostCheck: true,
    open: true,
  },
  productionSourceMap: false, // 生产打包时不输出map文件,增加打包速度
  chainWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      dllReference(config)
    }
  },
  configureWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      config.optimization.minimizer[0].options.terserOptions.compress.warnings = false
      config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true
      config.optimization.minimizer[0].options.terserOptions.compress.drop_debugger = true
      config.optimization.minimizer[0].options.terserOptions.compress.pure_funcs = ['console.log']
    }
  }
}
复制代码

有3个地方需要说明一下:

1、webpack.dll.config.js文件中的entry.vendor使用'vue/dist/vue.runtime.esm.js'作为vue的入口,是根据vue inspect > output.js的文件中resolve.alias决定的;(vue.runtime.esm.js还是vue.esm.js取决于vue create构建时的选择)

resolve: {
   alias: {
      '@': '/Users/saki_bc/bccgithub/vue-webpack-demo/src',
      vue$: 'vue/dist/vue.runtime.esm.js'
    },
}
复制代码

2、在开发环境中不使用dllPlugin是因为chrome的vue devtool是不能检测压缩后的vue源码,使得没办法用vue devtool观察vue项目的组件和数据状态;

3、add-asset-html-webpack-plugin插件必须在html-webpack-plugin之后使用,因此这里要用webpack-chain来进行配置;至于为什么'html'代表html-webpack-plugin,是因为@vue/cli-servide/lib/config/app.js里是用plugin('html')来映射的,关键源码片段如下:

const HTMLPlugin = require('html-webpack-plugin')
webpackConfig.plugin('html')
            .use(HTMLPlugin, [htmlOptions])
复制代码

4、这里不使用在index.html里添加script标签的方式引入dll文件,是因为当vue路由使用history模式,并且路由配置首页重定向到其他url的情况下,在首页刷新页面后dll文件会以重定向后的url的根目录引用,导致报错找不到dll文件。 如:dll的正确引用情况是http://www.xxx.com/vendor.dll.js,刷新重定向后变成 http://www.xxx.com/xxx/vendor.dll.js;即使在index.html使用绝对路径也是会出现这样的情况,目前还不知道是不是html-webpack-plugin的bug;

结语

这次优化实践仍然存在不少疑惑和且考虑的地方,webpack的更新发展也越来越快,vue-cli使用webpack-chain作为核心方式也增加了不少学习成本,接下来还需要阅读相关源码,发现项目中配置不合理的地方~

也希望各位大家能分享一下使用webpack4过程中踩到的坑~

相关文档

webpack-chain文档

add-asset-html-webpack-plugin文档

vue-cli配置源码