Vue 2.x 项目优化笔记

8/10/2021 前端Vue

以下 Vue CLI 的相关配置是在 Vue CLI 版本 >= 3.0 的基础上

# 包内容分析

Vue CLI 集成了 webpack-bundle-size-analyzer (opens new window) 插件,打包命令提供了 --report--report-json 两种命令参数来帮助我们分析包中包含的模块们的大小。

  • --report:生成 report.html 以帮助分析包内容
  • --report-json:生成 report.json 以帮助分析包内容

所以我们可以自定义一个 npm script,在 package.json 中的 scripts 属性值中追加:

"build:report": "vue-cli-service build --report --report-json",

想生成带有分析报告的打包文件时,敲 npm run build:report

report.html 打开后显示如下:

1

# 优化措施

# 路由懒加载

Vue Router 官网介绍 (opens new window)

原理是把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,提升加载速度。

使用示例:

const router = new VueRouter({
  routes: [{ path: '/foo', component: () => import('./Foo.vue') }]
})

// or

const Foo = () => import('./Foo.vue')
const router = new VueRouter({
  routes: [{ path: '/foo', component: Foo }]
})

# 异步组件

Vue 官网介绍 (opens new window)

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。异步组件契合按需加载的需求。

# 定义全局异步组件

Vue.component('async-webpack-example', function(resolve) {
  // 这个特殊的 `require` 语法将会告诉 webpack
  // 自动将你的构建代码切割成多个包,这些包
  // 会通过 Ajax 请求加载
  require(['./my-async-component'], resolve)
})

// or

Vue.component(
  'async-webpack-example',
  // 这个动态导入会返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

# 注册局部异步组件

new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }
})

# 生产环境禁止输出 SourceMap

SourceMap 文件可以使浏览器能够像调试源代码一样调试被混淆压缩后的 JavaScript 代码,所以在非生产环境,SourceMap 对于调试是有利的。

但是 SourceMap 文件有一定的安全隐患,有心人士可以通过 SourceMap 文件中的映射,更容易地还原出前端完整代码。

所以生产环境应该禁止输出 SourceMap,这样既可以加速生产环境的构建,又可以规避一部分信息安全问题。

Vue CLI 的 productionSourceMap 配置可以控制生产环境不输出 SourceMap 文件:

// vue.config.js
module.exports = {
  // 如果你不需要生产环境的 source map
  productionSourceMap: false
}

# 生产环境禁止日志打印和 debugger

Vue CLI 内置了 terser-webpack-plugin 插件,使用它可以控制是否移除日志打印和 debugger

# Vue CLI 3 配置方式

// vue.config.js

const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
  chainWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      config.optimization.minimizer([
        new TerserPlugin({
          terserOptions: {
            compress: {
              // 移除所有的日志打印
              // drop_console: true,
              // 移除所有的 debugger
              drop_debugger: true,
              // 该配置可以只移除一部分 log,但是必须设置 drop_console 为 false,如果值为 ['console.*'] 也是移除所有
              pure_funcs: ['console.log']
            }
          }
        })
      ])
    }
  }
}

# Vue CLI 4+ 配置方式

// vue.config.js

module.exports = {
  chainWebpack: config => {
    config.when(process.env.NODE_ENV === 'production', config => {
      config.optimization.minimizer('terser').tap(args => {
        // 移除所有的日志打印
        // args[0].terserOptions.compress["drop_console"] = true;
        // 移除所有的 debugger
        args[0].terserOptions.compress['drop_debugger'] = true
        args[0].terserOptions.compress['pure_funcs'] = ['console.log']
        return args
      })
    })
  }
}

# 常用依赖的优化

以下优化针对于没有使用 CDN 的依赖,如果以下依赖已经托管给了 CDN,可以忽略。

# Moment.js 2.x

Moment.js (opens new window) 是老牌的日期处理工具,正因为老牌,所以不能按需引入,其次通过分析图可见,打包后的 Moment.js 除了主包外,还有大量的语言包,占用体积比较大。

2

优化方案有两种:

第一种方案简单直接,Day.jsdate-fns 使用就不展开细讲了,感兴趣的可以移步官网。

下面讲讲第二种。

参考 《How to optimize moment.js with webpack》 (opens new window),可以使用 webpack.IgnorePlugin() 忽略 Moment.js 语言包,手动引入的语言包会被打包进 chunk-vendors 中。

// vue.config.js
const webpack = require('webpack')

configureWebpack: {
  plugins: [
    new webpack.IgnorePlugin({
      resourceRegExp: /^\.\/locale$/,
      contextRegExp: /moment$/
    })
  ]
}

//
chainWebpack: config => {
  config
    .plugin('ignore')
    // 忽略 moment/locale 下的所有文件,优化 moment 的体积
    // https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
    .use(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/))
}

假设我们只引入了简体中文包,效果如下:

import moment from 'moment'
import 'moment/locale/zh-cn'

moment.locale('zh-cn')

console.log('现在时间:', moment().format('MMMM')) // 现在时间: 八月

3

4

# Lodash 4.x

很多文章对于 Lodash 的优化,都是使用 lodash-webpack-plugin 这个插件,但是实际使用下来会有很多的问题,详情请见黑猫的《为什么你应该立即停止使用 lodash-webpack-plugin》 (opens new window)

并且在不出错的情况下 lodash-webpack-plugin 这个插件的效果与 import { head } from 'lodash-es'import head from 'lodash/head' 的效果几乎一致,它只是针对于 import { head } from 'lodash' 这种形式的引入做了优化处理。

// 整个 lodash 都会被打包,压缩后 72K
import { head } from 'lodash'
head([1, 2, 3])

// 打包压缩后 1K
import { head } from 'lodash-es'
head([1, 2, 3])

// 打包压缩后 1K
import head from 'lodash/head'
head([1, 2, 3])

得益于 ES6+ 的各种黑科技,其实在大多数情况下,我们对于 LodashUnderscore 等工具函数库的需求并不大,很多方法都有替代方案。详情请见 《You don't (may not) need Lodash/Underscore》 (opens new window)

所以我对于 Lodash 的优化建议是:

  • 如果需求不大就弃用;
  • 如果需求真的很大,就使用 lodash-es 或手动按需引入。
  • 或者等待模块化的 Lodash 现世

# ECharts 5.x

ECharts 的图表非常丰富,这也导致了它的全量包体积很大,如果全量引入:

echarts-all

有两种方式可以做到按需引入

# 1. 按需引入 ECharts 图表和组件

从 5.x 开始,官方提供了新的按需引入方式 (opens new window)

// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from 'echarts/core'
// 引入柱状图图表,图表后缀都为 Chart
import { BarChart } from 'echarts/charts'
// 引入提示框,标题,直角坐标系组件,组件后缀都为 Component
import {
  TitleComponent,
  TooltipComponent,
  GridComponent
} from 'echarts/components'
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from 'echarts/renderers'

// 注册必须的组件
echarts.use([
  TitleComponent,
  TooltipComponent,
  GridComponent,
  BarChart,
  CanvasRenderer
])

可以选择把 echarts 注册在 Vue 原型上

// main.js

import Vue from 'vue'

Vue.prototype.$echarts = echarts

来看一下效果:

echarts-compress

# 2. 使用官方的在线定制功能

如果你觉得按需引入的方式心智负担很重,也可以选择使用官方的在线定制功能 (opens new window)按需生成 echarts.min.js 文件。

echarts-compress

# CDN 优化

不要盲目的把所有依赖都更换成第三方 CDN 方式引入,我觉得这个优化点需要分情况讨论。

# 1. 假设公司有 CDN 服务器

第三方的 CDN 其实不一定可信,起码不可能一直可信,假设公司有自己的 CDN 服务器,那就最好了。

这时候除了 index.html 的所有静态资源(包括 jscss图片视频字体等)都可以部署到 CDN 服务器上,这样本地的静态资源和依赖该怎么引入还怎么引入,完全不需要变动,只需要打包的时候 Vue CLI 的 assetsDir 配置为 CDN 对应路径。

至于为什么不连 index.html 也一起放在 CDN,如果这个网站是公司内部使用,可以放;如果是对外的站点,一是让用户用 CDN 路径去访问站点不太合适,二是 index.html 放在域名对应的服务器可以更方便的做网关层面的操作,比如 IP 限流、反向代理等。

// vue.config.js

module.exports = {
  assetsDir: process.env.NODE_ENV === 'production' ? 'path/to/your/CDN' : ''
}

# 2. 假设公司 IT 部门很严格,屏蔽了第三方 CDN 站点

大公司的 IT 部门监管一般很严格,这时候就不要考虑使用第三方 CDN 了,我就遇到过本来能正常使用的 CDN,过段时间发现被 IT 部门屏蔽了。再说大型企业一般都有自己的 CDN 服务器。

# 3. 假设公司的服务器配置非常给力

可以不用考虑使用第三方 CDN。

# 4. 最后来讨论可以用 CDN 优化的场景

上面三种情况只是举例,具体是否使用 CDN,要综合评估加载速度和总包流量,还请自行考量。

哪些依赖可以使用 CDN 呢?我的思路是针对那些必须全量引入的依赖,比如 VueVue RouterVuexAxiosNProgressJQuery 等。

ElementAntd Vue 这些 UI 框架,我选择不使用 CDN,因为它们本身可以按需引入,并且如果使用 CDN,改变主题颜色等操作就会变得复杂。当然如果你觉得使用 CDN 带来的加载速度提升大大优于不使用 CDN,并且没有动态改变主题等需求,那当前可以走 CDN。

其次要考虑,开发环境要和生产环境得区分开来,开发环境还是要使用本地的依赖,不然没有代码提示太痛苦了,效率也会降低。

另外 Vue CLI 帮我们集成了 html-webpack-plugin (opens new window) 插件,我们可以基于该插件,只在生产环境使用 CDN。

vue.config.js 修改如下:

// vue.config.js

const CDN_CONFIGS = {
  externals: {
    vue: 'Vue',
    'vue-router': 'VueRouter',
    vuex: 'Vuex',
    axios: 'axios',
    nprogress: 'NProgress'
  },
  links: [
    'https://cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.css'
  ],
  scripts: [
    'https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.min.js',
    'https://cdn.bootcdn.net/ajax/libs/vue-router/3.2.0/vue-router.min.js',
    'https://cdn.bootcdn.net/ajax/libs/vuex/3.5.1/vuex.min.js',
    'https://cdn.bootcdn.net/ajax/libs/axios/0.21.0/axios.min.js',
    'https://cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.js'
  ]
}

module.exports = {
  chainWebpack: config => {
    config.when(process.env.NODE_ENV === 'production', config => {
      config.set('externals', CDN_CONFIGS.externals)

      config.plugin('html').tap(args => {
        args[0].USE_CDN = true
        args[0].links = CDN_CONFIGS.links
        args[0].scripts = CDN_CONFIGS.scripts
        return args
      })
    })
  }
}

index.html 中使用 EJS (opens new window) 的语法引入样式和脚本,如下:

<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
  <head>
    <!-- 其他代码... -->

    <!-- CDN 引入CSS -->
    <% if (htmlWebpackPlugin.options.USE_CDN) { %>
      <% htmlWebpackPlugin.options.links.forEach(function (link) { %>
        <link href="<%= link %>" rel="stylesheet" />
      <% }) %>
    <% } %>
  </head>

  <body>
    <!-- 其他代码... -->

    <!-- CDN 引入JS -->
    <% if (htmlWebpackPlugin.options.USE_CDN) { %>
      <% htmlWebpackPlugin.options.scripts.forEach(function (script) { %>
        <script src="<%= script %>"></script>
      <% }) %>
    <% } %>
  </body>
</html>

打包之后的 index.html 代码如下:

5

webpack 的 外部扩展 Externals (opens new window),让程序可以在运行时再去从外部获取扩展依赖。所以在使用 CDN 时,只要是定义好的外部扩展,项目中 import xxx from ‘xxx’ 的引入方式依然可以正常运行,不需要做任何改动。

# Gzip 压缩

Gzip 是一种用于文件压缩与解压缩的文件格式。以 Nginx 为例,有两种方式开启 Gzip。

# 利用 ngx_http_gzip_module 模块实时压缩

通过 ngx_http_gzip_module 模块拦截请求,并对需要做 Gzip 的资源进行实时压缩,会消耗 CPU 资源。

ngx_http_gzip_module 模块是 Nginx 默认集成的,不需要额外编译安装。

配置示例:

# 开启 Gzip
gzip on;

# 设置允许压缩的文件最小字节数
gzip_min_length 1k;

# 设置压缩级别,取值 1-9,数字越大压缩的越好,也更消耗 CPU 的资源和时间
gzip_comp_level 2;

# 设置允许进行压缩的文件类型。JavaScript 有多种形式。其中的值可以在 mime.types 文件中找到。
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;

# 是否在 HTTP header 中添加 Vary: Accept-Encoding,建议开启
gzip_vary on;

# IE 6 及以下 禁用
gzip_disable "MSIE [1-6]\.";

# 设置压缩所需要的缓冲区大小
gzip_buffers 4 16k;

# 设置压缩针对的 HTTP 协议版本
gzip_http_version 1.0 | 1.1;

# 利用 http_gzip_static_module 模块静态压缩

http_gzip_static_module 模块不消耗 CPU 资源,该模块需要额外安装。

开启很简单:

gzip_static  on;

http_gzip_static_module 模块需要我们实现准备好压缩好的资源。前端需要利用到 compression-webpack-plugin 这个 webpack 插件,在打包的时候同时生成 .gz 格式的文件。

  1. 安装 compression-webpack-plugin

不要安装版本 7.x 或者 8.x,会报以下错误:

TypeError: Cannot read property 'tapPromise' of undefined

只能安装低版本的 6.x 或 5.x:

npm i -D compression-webpack-plugin@6.x
# or
yarn add -D compression-webpack-plugin@6.1.1
  1. vue.config.js 配置:
// vue.config.js

const CompressionWebpackPlugin = require("compression-webpack-plugin")

module.exports = {
  chainWebpack: config => {
    config.when(process.env.NODE_ENV === 'production', config => {
      config.plugin('compression').use(
        new CompressionWebpackPlugin({
          filename: '[path][base].gz',
          algorithm: 'gzip',
          test: /\.js$|\.css$|\.html$/,
          // 仅处理大于此大小的资源(以字节为单位),
          threshold: 1024,
          minRatio: 0.8
        })
      )
    })
  }
}

压缩效果如下,所有大于 1kb 的资源都生成了同名的 .gz 文件:

1

# 实时压缩和静态压缩结合使用

实时压缩和静态压缩可以结合使用,效果更佳。Nginx 会首先尝试使用静态压缩,如果有则直接返回 .gz 的预压缩文件,否则尝试动态压缩。

gzip_static       on;

gzip              on;
gzip_min_length   1k;
gzip_comp_level   1;
gzip_types        text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;
gzip_vary         on;
gzip_disable      "MSIE [1-6]\.";
gzip_buffers      4 16k;
gzip_http_version 1.0 | 1.1;