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

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

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

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

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的源码vue

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配置以下:java

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的输出以下:node

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的配置,接下来记录一下我踩到的坑~webpack

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

minimizer数组新增了一个对象:git

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,因此再次打包就不会对未修改的文件进行检测。github

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

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

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'呢?但愿有大神能回答我这个疑惑!

2、使用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个地方须要说明一下:

一、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'
    },
}
复制代码

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

三、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])
复制代码

四、这里不使用在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配置源码

相关文章
相关标签/搜索