基于vue-cli3以及wepack4.0的vue项目升级

5,476 阅读9分钟

github: vue-cli3-example

文章内容

  • 升级前后构建速度对比
  • 如何构建基于 vue-cli3、webpack4.0 以及 vux 的vue项目
  • 简析 vue-cli-service 集成的webpack配置
  • 如何优化 webpack 配置,提升构建速度

构建速度对比

出于 构建速度 的考虑,最近把之前基于 vue-cli2.0 和 webpack3.x 的vue项目升级为 vue-cli3.0 和 webpack4.x,零配置下构建速度提升都已经很明显。

vue-cli

webpack3.12.0

vue-cli

webpack4.42.0

先看下升级前后 构建速度对比

构建 模式 升级前(s) 升级后(s) 提升比(%)
首次构建 dev 53.832 54.062 -0.43
pro 87.44 89.20 -2.01
二次构建 dev 3.965 1.165 70.62
pro 88.90 51.05 42.58
  • 升级前:vue-cli2、webpack: 3.12.0 优化过配置
  • 升级后:vue-cli3、webpack: 4.42.0 默认配置

我们可以看到升级后首次构建速度有些许下降,但二次构建速度显著提升。升级前product模式下,首次构建和二次构建速度并无太大差别,并且不快反而更慢了。

VUE-CLI3项目构建

1、安装 VUE-CLI3

如果没有安装 yarn,请先安装 yarn

yarn global add @vue/cli

2、使用图形界面

# 图形界面
vue ui
# 命令行交互
vue create

你也可以选择命令行交互进行构建,在这里使用图形界面方式构建,上述会打开一个浏览器窗口,并以图形化界面将你引导至项目创建的流程

vue-cli

创建界面

3、选择配置

根据项目需求选择对应配置,基于之前的项目,所以选择了以下配置

  • 包管理器:yarn
  • Babel
  • Router
  • Vuex
  • CSS-Pre-processors: less
  • Use history mode of router
  • Eslint(lint on save): standard config

4、运行项目

# serve
yarn serve
# build
yarn build

到这里基本的项目架构就搭建完成了。

5、迁移代码

迁移之前业务代码至 vue-cli 项目下,安装项目依赖的第三方包:axios、blueimp-canvas-to-blob、clipboard-copy、date-fns、vue-social-sharing、vuex-router-sync... 可根据编译错误提示进行操作

6、配置 VUX

这是本次迁移的难点。因为 vux 依赖 vux-loader 的支持,而且与最新版本的 vue-loader 不兼容,所以需要降级处理,以下是本项目选择的各依赖版本。官方作者说会兼容15.x版本。

  • vux-loader:^1.1.31
  • vue-loader:^14.0.0
  • vux:^2.8.1
# 安装依赖
yarn add vux@^2.8.1
yarn add vux-loader@^1.1.31  vue-loader@^14.0.0 -D 
// 配置vux-loader

// vue.config.js 这是webpack构建依赖 如果没有需要在根目录创建
module.exports = {
    // 相对于 outputDir: dist 的子目录
    assetsDir: 'static',
    // 配置 CopyWebpackPlugin vue-cli-service 已经内置
    // 按照之前项目要求 将根目录static文件 copy 至dist目录static子目录下
    chainWebpack: config => {
        config
        .plugin('copy')
        .tap(options => {
            options[0] = [{
                from: path.resolve(__dirname, 'static'),
                to: 'static',
                ignore: ['.*']
            }]
            return options
        })
    },
     // theme.less 为自定义全局样式 如果没有可不配置
    configureWebpack: config => {
        require('vux-loader').merge(config, {
        plugins: [
            { name: 'vux-ui' },
            { name: 'duplicate-style' },
            { name: 'less-theme', path: 'src/theme.less' }
        ]
    })
  }
}
// 修改入口 main.js 文件下vux组件或者插件引入方式,其他文件无需更改。这是 vue-loader 版本不兼容导致的
// 相关issue: https://github.com/airyland/vux/issues/2310

// import { LoadingPlugin, ToastPlugin, ConfirmPlugin,
//  XInput, Group, Toast, Confirm, AlertPlugin } from 'vux'

import LoadingPlugin from 'vux/src/plugins/loading'
import XInput from 'vux/src/components/x-input'

webpack配置简析

1、vue.config.js

可创建vue.config.js自定义webpack配置

基于vue-cli3,可以零配置使用webpack4来运行项目。所以,vue ui初始化完成的项目没有 webpack 的配置文件,但是依然可以像拥有 webpack 那样运行项目,这是因为 vue-cli-service 已经集成了 webpack 的功能。所以,我们不必像 vue-cli2x 那样必须针对不同的环境创建不同的配置 webpack.base.conf.js、webpack.test.conf.js、webpack.prod.conf.js。vue.config.js官方参考文档

vue-cli

vue.config.js

2、.env环境变量

经过 CLI 封装后仅支持注入环境配置文件中以 VUE_APP_ 开头的变量

我们可以在根目录下创建 .env 文件配置不同的环境变量


# 配置权重 .env.[mode].local > .env.[mode] > .env.local > .env
.env                # 在所有的环境中被载入
.env.local          # 在所有的环境中被载入,但会被 git 忽略
.env.[mode]         # 只在指定的模式中被载入
.env.[mode].local   # 只在指定的模式中被载入,但会被 git 忽略

3、默认webpack配置

其实这一步很重要,我们自定义的 webpack 配置都可以通过此方法来查看。

可通过 vue ui 界面查看 development、production 模式下 webpack 配置。 也可以通过 vue inspect > output.js --mode production 指定模式输出到文件查看。 但在这里,只是展开分析现有配置的plugin.

vue-cli

vue ui

vue-cli

vue inspect


/* 加载编译vue组件 允许你以单文件组件的格式编写 Vue 组件 */
const VueLoaderPlugin = require('vue-loader/lib/plugin');

// webpack内置插件
/*DefinePlugin:  将process.env注入到客户端代码中  */
/*NamedChunksPlugin: 自定义chunk名字 */
/*HotModuleReplacementPlugin: 热替换*/
/*ProgressPlugin: 自定义编译进度报告*/
/*HashedModuleIdsPlugin: 用于根据模块的相对路径生成 hash 作为模块 id*/
const { DefinePlugin, NamedChunksPlugin, HotModuleReplacementPlugin, ProgressPlugin, HashedModuleIdsPlugin } = require('webpack');

// 用于强制所有模块的完整路径必需与磁盘上实际路径的确切大小写相匹配
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');

// 识别某些类型的 webpack 错误并整理,以提供开发人员更好的体验。
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');

// 用于根据模板或使用加载器生成 HTML 文件
const HtmlWebpackPlugin = require('html-webpack-plugin');

// 用于在使用 html-webpack-plugin 生成的 html 中添加 <link rel ='preload'> 或 <link rel ='prefetch'>,有助于异步加载
const PreloadPlugin = require('preload-webpack-plugin');

// 用于将单个文件或整个目录复制到构建目录
const CopyWebpackPlugin = require('copy-webpack-plugin');

// 将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

// 用于在 webpack 构建期间优化、最小化 CSS文件
const OptimizeCssnanoPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
    plugins: [
        /* 公共 */
        /* config.plugin('vue-loader') */
        new VueLoaderPlugin(),
        /* config.plugin('define') */
        new DefinePlugin(),
        /* config.plugin('case-sensitive-paths') */
        new CaseSensitivePathsPlugin(),
         /* config.plugin('friendly-errors') */
        new FriendlyErrorsWebpackPlugin(),
         /* config.plugin('html') */
        new HtmlWebpackPlugin(),
         /* config.plugin('preload') */
        new PreloadPlugin({
            rel: 'preload',
            include: 'initial',
            fileBlacklist: [
              /\.map$/,
              /hot-update\.js$/
            ]
         }),
        /* config.plugin('prefetch') */
        new PreloadPlugin({
            rel: 'prefetch',
            include: 'asyncChunks'
        }),
        /* config.plugin('copy') */
        new CopyWebpackPlugin(),
    
        /* 生产模式 */
        /* config.plugin('extract-css') */
        new MiniCssExtractPlugin(,
        /* config.plugin('optimize-css') */
        new OptimizeCssnanoPlugin(),
        /* config.plugin('hash-module-ids') */
        new HashedModuleIdsPlugin(),
        /* config.plugin('named-chunks') */
        new NamedChunksPlugin(),
        
        /* 开发模式 */
        /* config.plugin('hmr') */
        new HotModuleReplacementPlugin(),
         /* config.plugin('progress') */
        new ProgressPlugin(),
    ]
}

4、构建速度分析

那么为什么升级 webpack4.0 之后,使用 vue-cli-service 默认的webpack配置竟可以带来如此大的提升呢?相比于之前 webpac3.x 的构建, 原因其实主要有以下几点。

cache-loader

我们截取部分的loader配置来看下

module: {
   {
        test: /\.vue$/,
        use: [
          'vux-loader',
          /* config.module.rule('vue').use('cache-loader') */
          {
            loader: 'cache-loader',
            options: {
              cacheDirectory: '/node_modules/.cache/vue-loader',
              cacheIdentifier: 'a85c2746'
            }
          },
          /* config.module.rule('vue').use('vue-loader') */
          {
            loader: 'vue-loader',
            options: {
              compilerOptions: {
                preserveWhitespace: false
              },
              cacheDirectory: '/node_modules/.cache/vue-loader',
              cacheIdentifier: 'a85c2746'
            }
          }
        ]
    }, 
    /* config.module.rule('js') */
      {
        test: /\.m?jsx?$/,
        exclude: [
          function () { /* omitted long function */ }
        ],
        use: [
          /* config.module.rule('js').use('cache-loader') */
          {
            loader: 'cache-loader',
            options: {
              cacheDirectory: '/node_modules/.cache/babel-loader',
              cacheIdentifier: '3fc38ba7'
            }
          },
          /* config.module.rule('js').use('thread-loader') */
          {
            loader: 'thread-loader'
          },
          /* config.module.rule('js').use('babel-loader') */
          {
            loader: 'babel-loader'
          }
        ]
      },
}

我们可以看到在编译 vue 文件或者 js 文件的时候会使用 cache-loader 将编译的结果缓存在磁盘 node_modules .cache 目录下,当二次编译的时候会优先从缓存来取,减少 vue-loader、babel-loader 的构建时间,二次编译时间会缩短很多。vue-cli-service使用的 webpack 配置在cache的处理上有很多。

parallel(thread-loader)

生产模式下 webpack 有个默认的配置 parallel作用是为 Babel 或 TypeScript 使用 thread-loader,该选项在系统的 CPU 有多于一个内核时自动启用。相比于单进程处理编译,多进程编译的速度也会提升很多

module.exports = {
  parallel: require('os').cpus().length > 1,
/* config.module.rule('js') */
  {
    test: /\.m?jsx?$/,
    exclude: [
      function () { /* omitted long function */ }
    ],
    use: [
      /* config.module.rule('js').use('thread-loader') */
      {
        loader: 'thread-loader'
      }
    ]
  },
}

optimization

webpack4.0支持 optimization 设置,替代了 commonChunksPlugin。 该功能在生产模式下默认开启,告知 webpack 使用 TerserPlugin 压缩 bundle。 压缩过程支持缓存,多进程,所以压缩速度也会提升很多。

module.exports = {
    optimization: {
        minimize: true,
        minimizer: [
            new TerserPlugin({
                cache: true,
                parallel: true,
                sourceMap: true, // Must be set to true if using source-maps in production
                terserOptions: {
                  // https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
                }
              }),
            ],
        }
    }
}

优化 webpack 配置

vue-cli3 下的 webpack 主要在构建过程中使用 cache、parallell 来提升编译速度,我们也可以继续强化这些构建配置,但是,更重要的是,这给我们如何从零配置webpack而又能获得速度上的提升指引了方向,所以,即使无法进行项目升级,但是在现有配置基础上进行优化仍然可以取得速度上的提升。

所以我们可以如何提升构建速度?我们先看下配置工程中可以使用的一些工具。

社区很多工具提供了可视化分析,在这里使用 webpack-bundle-analyzer来查看资源大小信息、speed-measure-webpack-plugin来看打包过程中各个插件、loader消耗的时间,webpack内置插件 progress-plugin(development模式下会默认开启) 查看打包各个过程消耗的时间

vue-cli

webpack-bundle-analyzer

vue-cli

progress-plugin

vue-cli

speed-measure-webpack-plugin

使用方法:

// vue.config.js
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const { ProgressPlugin } = require('webpack')


module.exports = {
    chainWebpack: config => {
        if (process.env.NODE_ENV === 'development') {
          config
          .plugin('webpack-bundle-analyzer')
          .use(BundleAnalyzerPlugin)
          .end()
        }
        
        config
        .plugin('speed-measure-webpack-plugin')
        .use(SpeedMeasurePlugin)
        .end()
        
        // 在development模式下会默认开启,但是profile默认不开启,所以我们自行重新设置下
        // 也可以配置package.json 在执行命令增加--progress --profile
        // "build": "vue-cli-service build --progress --profile",

        config
        .plugin('progress')
        .use(ProgressPlugin)
        .tap(options => {
          options = [{
            // handler (percentage, msg) {
            //   console.info((percentage.toFixed(2) * 100) + '%', msg)
            // },
            profile: true,
          }]
          return options
        })
    }
}

// 非vue-cli3项目
const config = {
    ...,
    plugins: [
        new BundleAnalyzerPlugin(),
        // 或者运行时通过参数设置开启 webpack --config webpack.prod.config.js --progress --profile
        new ProgressPlugin({
            profile: true
        })
    ],
    ...
}
// 如果需要配置 SpeedMeasurePlugin 可创建 webpack.speed.js 将需要分析的配置引入 再执行分析 webpack --config webpack.speed.js
const config = require('./webpack.prod.config.js')
const smp = new SpeedMeasurePlugin()
module.exports = smp.wrap(config)

1、使用 DllPlugin 拆分模块

通过 webpack-bundle-analyzer 分析,我们发现项目注入的 UI 组件库特别大(6.19MB),所以,我们可以把不需要经常更新的依赖单独抽出来打包并放到 cdn 上,这有利于提高性能,更能避免每次都打包这些资源以提升构建速度。

// 创建 build/webpack.dll.js

const webpack = require('webpack')
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
// 抽离的项目依赖
const vendors = [
  'vux',
  'axios',
  'vue-router',
  'vuex',
  'vuex-router-sync',
  'dateformat',
  'date-fns',
  'html2canvas'
]
const dllConfig = {
  mode: 'production',
  entry: {
    vendors: vendors,
  },
  // dll输出目录
  output: {
    path: path.join(__dirname, '..', 'dll'),
    filename: '[name].[chunkhash].dll.js',
    library: '[name]_[chunkhash]',
  },
  resolve: {
    extensions: ['.js', '.vue', '.json', '.scss'],
  },
  module: {
    rules: [{
      test: /\.vue$/,
      loader: 'vue-loader',
    },  {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      loader: 'url-loader',
      query: {
          limit: 10000,
          name: 'img/[name].[hash:7].[ext]'
      }
    }, {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        loader: 'url-loader',
        query: {
            limit: 10000,
            name: 'fonts/[name].[hash:7].[ext]'
        }
    }]
  },
  plugins: [
    new CleanWebpackPlugin(),
    // dll输出文件*.manifest.json *.js
    new webpack.DllPlugin({
      path: path.join(__dirname, '..', 'dll', '[name].manifest.json'),
      name: '[name]_[chunkhash]'
    }),
    // new BundleAnalyzerPlugin()
  ],
  // optimization: {
  //  minimize: false
  // }
}
// 因为需要抽离第三方vux组件库,所以这里需要依赖vux-loader
module.exports = require('vux-loader').merge(dllConfig, {
  plugins: [{
      name: 'vux-ui'
  }]
})

在抽离项目依赖时,使用了默认的 optimization 进行资源压缩。将 optimization minimize 属性设置成 false,关闭压缩。可以看下压缩前后资源对比。

  • 压缩前:size: 5.24MB, gzip: 881.73KB
  • 压缩后:size: 2.78MB, gzip: 551.74KB
vue-cli

压缩前

vue-cli

压缩后


// 因为 webpack4.0 中 webpack 命令行操作已经单独抽成 webpack-cli 所以运行 webpack 需要安装开发依赖 "webpack-cli": "^3.3.11"
"devDependencies": {
    "webpack-cli": "^3.3.11"
},
// package.json添加构建命令
"scripts": {
    "build-dll": "webpack --config build/webpack.dll.config.js --mode production",
}

运行 yarn build-dll 命令后就会在 dll 目录下生成 dll.js 文件和对应的 manifest.json 文件 使用 DLLReferencePlugin 引入,并通过 AddAssetHtmlPlugin 将资源引入 index.html

// vue.config.js
const DllReferencePlugin = require('webpack').DllReferencePlugin
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')

module.exports = { 
  chainWebpack: config => {
    config
    .plugin('dll')
    .use(DllReferencePlugin)
    .tap(options => {
      options[0] = {
        // 上一步操作生成的manifest.json文件
        manifest: path.join(__dirname, 'dll', 'vendors.manifest.json')
      }
      return options
    })
    
    // 我们在这里可以先移除preload | prefetch插件
     // 移除 prefetch
    config.plugins.delete('prefetch')
    // 移除 preload
    config.plugins.delete('preload')
    
    config
    .plugin('asset')
    .use(AddAssetHtmlPlugin)
    .tap(options => {
      options[0] = {
        filepath: path.resolve(__dirname, 'dll/*.js'),
        // dll 引用路径
        publicPath: './vendor',
        // dll最终输出的目录
        outputPath: './vendor'
      }
      return options
    })
    .end()
  }
}

// 非vue-cli3项目
module.exports = {
    ...,
    plugins: [
       new DllReferencePlugin({
        manifest: path.join(__dirname, 'dll', 'vendors.manifest.json')
       }),
       new AddAssetHtmlPlugin({
        filepath: path.resolve(__dirname, 'dll/*.js'),
        // dll 引用路径
        publicPath: './vendor',
        // dll最终输出的目录
        outputPath: './vendor' 
       })
    ],
    ....
}

因为之前依赖的 vux 资源比较大,所以这次抽离对项目构建速度有很大的提升。构建速度提升到了 39.34 秒。

vue-cli

2、适当使用parallel

  • 多进程 loader 转换

默认配置有很多 loader 转换已经使用 thread-loader,从单一进程的形式扩展为多进程的模式,从而加速代码构建。

HappyPack与vue-loader不兼容,我们延续默认的 thread-loader。但是默认的配置职位 Babel 或 Typescript 开启了多进程,是否还可以为其他地方增加 thread-loader。我们来尝试一下。

// vue.config.js
const threadLoader = require('thread-loader')
threadLoader.warmup({}, [
  'vue-loader',
  'eslint-loader'
])

module.exports = {
    chainWebpack: config => {
        config.module.rule('eslint')
          .use('thread-loader')
          .loader('thread-loader')
          .before('eslint-loader')
    
        config.module.rule('vue')
          .use('thread-loader')
          .loader('thread-loader')
          .before('vue-loader')
    }
}

// 非vue-cli3项目
const threadLoader = require('thread-loader')
threadLoader.warmup({}, [
    'babel-loader',
    'vue-loader',
    'eslint-loader'
])
module.exports = {
    module: {
        rules: [
            {
                enforce: 'pre',
                test: /\.(vue|(j|t)sx?)$/,
                use: [
                    'thread-loader',
                    {
                        loader: 'eslint-loader',
                        options: {
                          cache: true,
                          cacheIdentifier: '21eefa8d',
                          emitWarning: true,
                          emitError: false
                        }
                    }
                ]
            },
            {
                test: /\.js$/,
                use: ['cache-loader', 'thread-loader', 'babel-loader'],
                exclude: /node_modules/
            },
            {
                test: /\.vue$/,
                use: [
                    'thread-loader',
                    {
                        loader: 'vue-loader',
                        options: {
                          compilerOptions: {
                            preserveWhitespace: false
                          },
                          // 根据目录路径设置
                          cacheDirectory: path.join(__dirname, 'node_modules', '.cache', 'vue-loader') 
                          cacheIdentifier: '50c60676'
                        }
                    }
                ]
            }
        ]
    }
}

使用后发现,构建速度变得更慢了。thread-loader 依赖 CPU 性能,如果过多使用反而会降低打包速度,所以在此项目下不再使用 thread-loader。但是,多进程构建模式可以作为我们优化 webpack 构建速度的一个方向,在进行配置优化的时候,这是一个很好的优化点

  • 多进程并行压缩代码
// 非vue-cli3项目
// 并不一定需要使用TerserPlugin 可以使用webpack4默认压缩
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
    ...,
    optimization: {
        minimizer: [
          new TerserPlugin({
            cache: true,
            parallel: true,
            sourceMap: true,
            exclude: /node_modules/,
          })
        ]
    }
}

3、缓存loader的执行结果(cache-loader)

如在构建速度分析章节中提到的那样,在默认的配置中,webpack已经做好了足够的缓存策略,所以这里也没有很大的提升空间了。但是,如果在非vue-cli3项目中,我们可以尝试设置去提升优化,具体的可参考vue-cli3配置。

// 非vue-cli3项目
const path = require('path')
module.exports = {
    module: {
        rules: [{
            test: /\.m?jsx?$/,
            use: [{
                loader: 'cache-loader',
                options: {
                  cacheDirectory: path.resolve(__dirname, 'node_modules', '.cache', 'babel-loader')
                  cacheIdentifier: '5b00f796'
                }
            }]
        }]
    }
}

4、优化模块查找路径(include,exclude)

webpack在打包过程中会根据包名进行查找,这样也会增加查找时间,如果能够指定更具体的查找目录,就可以有效降低查找时间。

我们可以对搜索过程进行一些优化,比如可以像下面这样指定路径

exclude: /node_modules/, // 排除不处理的目录
include: path.resolve(__dirname, 'src') // 精确指定要处理的目录
resolve: {
    modules: [path.resolve(__dirname, 'node_modules')], 
    // 指定node_modules的位置
    alias: {
      '@': __dirname + '/src',
    }
}

所以在项目增加下面的一些模块限制

module.exports = {
    chainWebpack: config => {
         Array.from(['src', 'node_modules/vux-loader', 'node_modules/vue-core-image-upload', 'node_modules/vue-svg-icon']).forEach(item => {
          config.module.rule('vue').include.add(__dirname + `/${ item }`)
        })
        config.module.rule('images').include.add(__dirname, '/src')
    }
}

可是,也没有给构建速度带来很大的提升。但是,随着项目的逐渐增大,增加这些具体的路径,也可以有效缩短模块查找时间。