vue-cli4 (Vue 2.x) 项目打包优化方案

随着vue cli升级到 4 (内置webpack 4),我们需要手动做的优化就越来越少了。
通过vue-cli 4.x,也就是@vue/cli成功构建一个项目的时候,它不仅自动安装好了必要的库和插件,而且做完了针对大部分应用的 webpack 优化配置。
我们可以用 inspect 命令 审查解析好的配置文件 (用了什么 loaders 和 plugins 会比较直观),也可以看@vue/cli-service这个包,即/node_modules/@vue/cli-service/lib/config/目录下的文件(逻辑条理更清晰,多看源码也有好处),便可大致清楚它默认帮我们做了什么配置。

审查项目的 webpack 配置

在 shell 执行vue inspect > defaultConfig.js,就可以把我们现有的webpack配置解析成一个json对象并输出到目标js文件里。
其中用 webpack-chain 方式配置的模块规则,

.rule('vue')
  .test(/\.vue$/)
    .use('cache-loader')
      .loader('cache-loader')
      .end()
    .use('vue-loader')
      .loader('vue-loader')

会被转化成如下的 module.rules 配置。相对而言更容易理解。

module: {
  rules: [  /* config.module.rule('vue') */
    {
      test: /\.vue$/,
      use: ['cache-loader','vue-loader']
    },
  ]
}
直接用 vue ui 查看也可

vue-cli4 的默认 webpack 配置

经过对@vue/cli-service源码的分析,将 vue-cli4 默认打包配置梳理如下(非面面俱到):

1. config/app.js
  • 在 output 选项配置打包输出的 bundle 文件名为[name].[contenthash:8].js的格式,并定义它们的输出目录:打包输出目录/静态资源目录/js,如dist/static/js

  • 非测试环境下,通过optimization.splitChunks自定义代码分割逻辑,先把初始依赖的node_modules包提取到chunk-vendors.js文件,再把被不同入口的公共模块提取到chunk-common文件。

  • 配置 wepack 内置的NamedChunksPlugin插件,使 chunk id 保持稳定,如此异步 chunks 也能有始终如一的哈希值。

  • 通过 @vue/preload-webpack-plugin 插件,将在 index.html 引入的 js、css 的标签上,加上rel='preload'

  • CorsPlugin给 html 配置 crossorigin 属性(在vue.config.js自行配置了crossorigin的值才会生效)

  • copy-webpack-plugin插件把 public 目录的文件复制到定义的outputDir(打包目录,默认是dist)下

2. config/base.js
  • 通过 module 模块给不同文件类型配置不同预处理器。
    .vue(单文件组件):vue-loader (为每个语言块(如<template><script>)生成一个模块导入,并将遇到的资源URL都转为 webpack 模块请求。具体 ➡️ vue-cli4 之 vue-loader 工作流程)、cache-loader(一些性能开销较大的 loader (如vue-loader)可以链式添加 cache-loader,等 vue-loader 处理完再由它开启基于文件系统的模板编译缓存,以便于将结果缓存在磁盘中以减少编译时间)
    images(图片类型,png|jpeg等)、media(媒体类型,mp4|mp3|wav等)、fonts(字体类型,eot|ttf等):用 url-loader 导出为 内联 base64 URI,超过 limit 值用 file-loader 处理,发送到打包目录下的静态资源相应文件夹并返回访问 URL(具体 ➡️ file-loader 配置详解以及资源相对路径处理)
    svg(svg格式,通常是图标):file-loader
    定义它们构建后的文件名格式为[name].[hash:8].[ext],以及它们的输出目录:dist/静态资源目录/[js|img|media|fonts],如dist/static/img

  • 通过resolve选项修改模块解析配置,*.jsx*.vue等文件引入时不用再加后缀;定义@src目录绝对路径的别名;配置解析模块时搜索的目录为node_modules;不解析vue|vue-router|vuex|vuex-router-sync模块

  • terser-webpack-plugin (内置uglify-js可以自定义用 uglify-js覆盖默认的 minify 函数来进行压缩) 插件对JS进行了压缩和 treeshaking,我们基本可以不用考虑代码混淆的事,也不必额外安装和配置 uglifyjs-webpack-plugin 了。
    如果要在生产环境删除console.log的话,我偷懒就直接在terser的配置文件(config/terserOptions.js)的compress对象里加上了drop_console: process.env.NODE_ENV === 'production' ? true : false,

3. config/css.js
  • 对构建输出的 css 的文件名使用[name].[contenthash:8].js的格式,同时设置它们的输出目录:dist/静态资源目录/css,如dist/static/css

  • 按引入方式和是否开启 modules,对各种 css 语言配置了预处理规则,并应用相应的 loader 去做处理。包括 postcss、css、scss、sasslessstylus
    非生产环境会先经过 postcss-loader、css-loader(会把 css 中的资源URL转为模块请求),最后应用 vue-style-loader 往 <head> 标签中注入多个 <style> 标签。如果是 sass 类型,则在它们之前先经过sass-loader。
    关于样式处理,【vue-cli4 之 vue-loader 工作流程】这篇也涉及不少。

  • css.loaderOptions.postcss 配置开启了 autoprefixer,因此开发过程中我们使用无前缀的 CSS 规则即可。

  • 在生产环境,经过 postcss-loader、css-loader 等处理之后,用 mini-css-extract-plugin 把 css 提取成一个个单独的 css 文件,并配置该插件的 publicPath (css 中引用外部资源URL的公共前缀) 为输出的 *.css 文件基于打包目录的相对路径。 optimize-cssnano-plugin 负责对 css 进行压缩。
    关于mini-css-extract-plugin 插件的配置可能这里说得比较拗口,可移步 ➡️ 【file-loader 配置详解以及资源相对路径处理】,里面有详细的分析说明。

4. config.prod.js
  • 生产环境下,关闭资源地图(sourceMap)以提高构建速度和避免资源被定位到原始资源。不用再手动设置productionSourceMap: false

  • 配置 webpack 内置的HashedModuleIdsPlugin,使得打包时只改变 修改/新增 模块的哈希值,即未改动的文件名中的 hash (id)不变,从而维持未修改文件的缓存,以实现访问速度的提高。

  • 在测试环境关闭optimization.minimize压缩优化,以提高构建的速度

现在要来说我们还能做什么优化处理

优化的目标始终是明确的:减少项目体积、提高初次/首屏加载速度、尽可能利用浏览器缓存。

1. 公共代码及第三方库等抽离/分割

@vue-cli-service 默认给我们做了这样的配置:

// /node_modules/@vue/cli-service/lib/config/app.js
chainWebpack(webpackConfig => {
  if (process.env.NODE_ENV !== 'test') {
    webpackConfig
      .optimization.splitChunks({
          cacheGroups: {
            vendors: {
              name: `chunk-vendors`,
              test: /[\\/]node_modules[\\/]/,
              priority: -10,
              chunks: 'initial'
            },
            common: {
              name: `chunk-common`,
              minChunks: 2,
              priority: -20,
              chunks: 'initial',
              reuseExistingChunk: true
            }
          }
        })
    }
}

【webpack SplitChunksPlugin 配置详解】
【webpack SplitChunksPlugin vue-cli 4 拆包实战】
代码分包对缓存控制和请求响应速度的影响至关重要,了解 chunk 优化是在什么阶段处理也很有必要,详细看👆这两篇。为了篇幅这里直接甩配置:

// vue.config.js
chainWebpack: config => {
  if (IS_PROD) {
    config.optimization.splitChunks({
      chunks: 'all', // 表明选择哪些 chunk 进行优化。通用设置,可选值:all/async/initial。设置为 all 意味着 chunk 可以在异步和非异步 chunk 之间共享。
      minSize: 20000, // 允许新拆出 chunk 的最小体积
      maxAsyncRequests: 10, // 每个异步加载模块最多能被拆分的数量
      maxInitialRequests: 10, // 每个入口和它的同步依赖最多能被拆分的数量
      enforceSizeThreshold: 50000, // 强制执行拆分的体积阈值并忽略其他限制
      cacheGroups: {
        libs: { // 第三方库
          name: 'chunk-libs',
          test: /[\\/]node_modules[\\/]/, // 请注意'[\\/]'的用法,是具有跨平台兼容性的路径分隔符
          priority: 10 // 优先级,执行顺序就是权重从高到低
          chunks: 'initial' // 只打包最初依赖的第三方
        },
        elementUI: { // 把 elementUI 单独分包
          name: 'chunk-elementUI',
          test: /[\\/]node_modules[\\/]element-ui[\\/]/,
          priority: 20 // 权重必须比 libs 大,不然会被打包进 libs 里
        },
        commons: {
          name: 'chunk-commons',
          minChunks: 2, // 拆分前,这个模块至少被不同 chunk 引用的次数
          priority: 0,
          reuseExistingChunk: true
        },
        svgIcon: {
          name: 'chunk-svgIcon',
          // 函数匹配示例,把 svg 单独拆出来
          test(module) {
            // `module.resource` 是文件的绝对路径
            // 用`path.sep` 代替 / or \,以便跨平台兼容
            // const path = require('path') // path 一般会在配置文件引入,此处只是说明 path 的来源,实际并不用加上
            return (
              module.resource &&
              module.resource.endsWith('.svg') &&
              module.resource.includes(`${path.sep}icons${path.sep}`)
            )
          },
          priority: 30
        }
      }
    })
  }
}

这里提一嘴,如果用到的组件的比较多,就推荐完整引入 element-ui 库。否则这种情况按需引入打包出来并不会小。具体来说就是按照官方的按需引入操作,用到的 element-ui 代码也都是入口 chunk 的初始依赖。根据我们的优化配置再单独抽到一个包里,import 组件多的情况,区别不大。若是手动在页面 import element 组件,一般 UI 库组件利用率比较高,单独包含在懒加载页面里会重复,还是打包在公共 chunk 里合适。所以初始化时就 import 更好。
而且 element-ui 并不支持 treeshaking, 一是由于它并没有设置 sideEffects,二是 element-ui 的聚合模块中, 有一个注册所有组件为全局组件的副作用, 这会导致 tree shaking 失效。所以不用多折腾去考虑如何进一步优化 element-ui 包。

webpack 的处理流程是先收集处理依赖 ➡️ 标记无引用的模块 ➡️ 生成初步的 chunk (也就是分包) ➡️ 然后进一步优化这些 chunk ➡️ 最后 treeshaking。

2. 把部分第三方库用CDN外链的方式引入

Tip:通过 CDN 引入的资源不会被 webpack 打包,可以大大增加构建的速度。但这并不能真正意义上地减少项目体积,因为只是把部分代码拆出去,用别的方式引入罢了。想减小体积,最高效的方案是启用GZIP(后面第4点会详细说)。

其实我的项目没有必要单独剥离部分第三方依赖。一是构建速度没有什么问题,二是目前 webpack 的optimization.splitChunks 对静态资源的缓存优化已经做得很好了。并且我们可以把所有的静态资源都会上传到自己的 CDN 服务(阿里云、腾讯云、七牛等等都可),就没有必要使用第三方 CDN 服务了。BUT,跟splitChunks拆包一样,所有的优化都是需要结合自己的具体业务来调整,适合自己的才是最好的。(就算这次不用,我们会还是要会的【机智脸】)

我们借助 html-webpack-plugin 插件来优化cdn资源的引入。它的作用之一是动态添加script、link每次编译后的hash值,防止引用到被缓存的外部文件。

2.1 在 vue.config.js 导出模块module.exports = {}的外部定义 CDN 外链路径和externals对象

有依赖关系的外链顺序不能乱,比如vue一定要放在element-ui前面(虽然很常识)
externals (外部扩展配置) 告诉 webpack 不要打包哪些模块以及引入后的全局变量名

// 样式和js的CDN外链,会插入到index.html中
const cdn = {
  // 开发环境
  dev: {
    css: [],
    js: []
  },
 // 生产环境
  build: {
    css: ['https://cdn.jsdelivr.net/npm/element-ui@2.15.1/lib/theme-chalk/index.css'],
    js: [
      'https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js',
      'https://cdn.jsdelivr.net/npm/element-ui@2.15.1/lib/index.js'
    ]
  }
}

// 外部扩展,即外部引入对象与内部引用时的对象配置
// 例如:vue: 'Vue', 对应 import Vue from 'vue' 来说
// 属性名 vue 为要从外部引入时的 vue 对象,Vue为引入后的对应的全局变量。
const externals = {
  vue: 'Vue',
  'element-ui': 'ElementUI'
}
2.2 在 vue.config.js 的 webpack config 中加上external配置,并通过 html-webpack-plugin 把这些外链注入到 index.html之中
configureWebpack: config => {
  if (IS_PROD) { // 生产环境
    // 外部扩展配置,在production模式下,引入外部cdn资源,同时不要把这些模块打包到libs公共包里
    config.externals = externals
  } else {
    // 为开发环境修改配置
  }
},
chainWebpack: config => {
  if (IS_PROD) {
    // 添加 cdn 参数到 htmlWebpackPlugin 配置中
    config.plugin('html').tap(args => {
      args[0].cdn = cdn.build
      return args
    })
  }
}
2.3 在 public/index.html 里加上如下代码
<!-- 使用CDN的 CSS 文件 -->
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
<link rel="preload" href="<%= css %>" as="style" />
<link rel="stylesheet" href="<%= css %>" />
<% } %>
<% } %>
<!-- 使用CDN的 CSS 文件 end -->

<!-- 使用CDN加速的 JS 文件 -->
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
<script type="text/javascript" src="<%= js %>" ></script>
<% } %>
<% } %>
<!-- 使用CDN加速的 JS 文件 end -->

具体位置如下图。至于这里用到的模版引擎语法:看这里 ➡️ ejs模版语法

public/index.html
2.4 把入口文件 main.js 的 element-ui 相关引入导入注掉
import Vue from 'vue'
// import ElementUI from 'element-ui'
// import 'element-ui/lib/theme-chalk/index.css'
// Vue.use(ElementUI)

3. 静态图片压缩处理

看这个就行: 【vue-cli4 用 image-webpack-loader 配置 webpack 图片压缩处理/优化】

4. 开启gzip构建压缩 (服务器也要做相应配置)

包括gzip压缩原理的完整版 ➡️ 【如何为项目开启gzip压缩及实现原理】

用到的是 webpack 的这个插件:compression-webpack-plugin
我bulid的时候报了Cannot read property 'tapPromise' of undefined的错,其实就是版本和vue-cli的某些包不兼容,把 compression-webpack-plugin 的版本降低到6.1.1就可以了。
先安装npm install compression-webpack-plugin -D,然后到vue.config.js配置:

const CompressionPlugin = require("compression-webpack-plugin");

configureWebpack: config => {
    config.name = name
    const plugins = []
    if (IS_PROD) { // 生产环境
      plugins.push( 
        // 为静态资源准备压缩版本,在服务器也要开启相应配置
        new CompressionWebpackPlugin({
          test: /\.(js|css|json|ico|svg)$/,// 匹配文件格式
          algorithm: 'gzip',
          threshold: 10240, // 对超过10k的数据压缩
          minRatio: 0.8, // 压缩比
          filename: "[path][base].gz", // 压缩后的文件名,默认值是 [path][base].gz
          // filename(pathData) {
            // `pathData` 参数包含很多可以获取到文件路径相关数据的属性 - `path`/`name`/`ext`/等等
            // 如果路径中包含svg,则放到svg/目录下
            // 只是演示,一般都用字符串默认值就好
            // if (/\.svg$/.test(pathData.ext)) {
              //return 'static/svg/[base].gz'
            }
            // return '[path][base].gz'
          },
          deleteOriginalAssets: false, // 不删除源文件,true 则只保留压缩后的文件
        })
      )
    } else {
      // 为开发环境修改配置
    }
    config.plugins = [...config.plugins, ...plugins]
  },

在服务端的 nginx.conf 开启 gzip压缩配置,配置参数可参考:Nginx的gzip配置文档

    # 开启gzip压缩
    gzip on;
    gzip_buffers 4 16k; # 置用于压缩响应的缓冲区的数量和大小
    gzip_comp_level 9;  # 对响应压缩的级别,可选范围:1到9,数字越大压缩得越好,但也越占用CPU时间
    gzip_http_version 1.1; # 默认 1.1,请求压缩响应所需的最小HTTP版本
    gzip_min_length  1k;  # 设置被gzip的响应的最小长度,小于该值的文件不会被压缩
    # 追加启用gzip压缩的MIME类型,默认已有text/html
    gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/javascript application/json;
gzip_disable "MSIE [1-6]\.";
    gzip_vary on; # 默认off,如果指令gzip、gzip_static或gunzip是active的,启用插入" Vary: Accept-Encoding "响应报头字段

具体配置位置展示

5. 其他

测试阶段可以加个打包分析插件,视图分析更直观:webpack-bundle-analyzer
安装:npm install -D webpack-bundle-analyzer

chainWebpack: config => {
  // 添加插件
  // 注意链式配置webpack,不用再new去创建一个插件了,这件事已经默认帮我们做好了。
  config.plugin('webpack-bundle-analyzer').use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
}

6. 我个人完整的vue.config.js配置,供参考


未打包前的体积(开发模式)
默认配置打包后的体积
经过以上配置后打包后的体积
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容