vuecli3+webpack4優化實踐(刪除console.log和配置dllPlugin)

sakibcc發表於2019-03-10

本文主要介紹如何在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配置原始碼

相關文章