Skip to content
On this page

vue.config.js

首先说,其他章节提到的与 vue.config.js 相关的技术方案,本章就不再赘述。

然后说,如今 Vue CLI 5 和 webpack 5 技术都已经非常成熟,所以使用公共 CDN 和异步组件才是开发效率和访问速度的最大提升法则,而优化 vue.config.js 并不能为开发效率和访问速度带来明显的提升,所以前端开发者没有必要把精力放在优化 vue.config.js 上,按本文配置足矣。

最后说,Vue CLI 5.0 之后,vue.config.js 导出的是defineConfig({...}),本文为简单起见继续使用 Vue CLI 4.5 的module.exports = {...},道理是相同的。

设置资源目录

js
module.exports = {
  assetsDir: 'static',
};

关闭生产环境 source map

js
module.exports = {
  productionSourceMap: false,
};

开启 HTTPS

  1. 在工程根目录创建/ssl目录,在里面执行 3 条命令,其中第 2 条会询问问题,直接回车即可。
bash
openssl genrsa -out privatekey.pem 1024
bash
openssl req -new -key privatekey.pem -out certrequest.csr
bash
openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem
  1. 配置 vue.config.js:
js
module.exports = {
  devServer: {
    // Vue CLI 4.5 以前
    https: {
      key: fs.readFileSync(path.join(__dirname, './ssl/privatekey.pem')),
      cert: fs.readFileSync(path.join(__dirname, './ssl/certificate.pem')),
    },
    // Vue CLI 4.5 以后
    server: {
      type: 'https',
      options: {
        key: fs.readFileSync(path.join(__dirname, './ssl/privatekey.pem')),
        cert: fs.readFileSync(path.join(__dirname, './ssl/certificate.pem')),
      },
    },
  },
};

devServer

一个典型的配置代码如下:

js
  devServer: {
    port: 5418,
    open: true,
    proxy: {
      [process.env.VUE_APP_BASE_API]: {
        target: `http://www.dev.com`,
        pathRewrite: {
          ['^' + process.env.VUE_APP_BASE_API]: '',
        },
        changeOrigin: true,
      },
    },
    // Vue CLI 4.5
    disableHostCheck: true,
    // Vue CLI 5.0
    historyApiFallback: true,
    // Vue CLI 5.0
    allowedHosts: 'all',
  },

原理

没有设置devServer的前提下,由于所有 Ajax 请求的地址都是相对路径,所以都会发送到本机,但本机显然不是服务器,所以所有请求都会失败。现在有了devServer,Vue CLI 自己架设了一个转发服务器(webpack-dev-server),发向本机的请求全部会被转发到真正的服务器,真正的服务器返回内容又会发送给 webpack-dev-server,webpack-dev-server 再把内容发给前端,这样就可以正常调试了。

port

port: 5418,这个数字应尽量复杂,且全公司尽量唯一,因为程序员浏览器会记忆测试账号和它的密码,随着开发的项目越来越多,如果统一用8080端口,那么会导致账号输入框积攒越来越多的账号密码,增加心智负担,影响开发效率。

open

open: true,帮你自动打开浏览器,应设。

proxy

键名

键名就是正式服务器的 API 的相对路径或一部分,它用来跟实际请求的 API 路径进行匹配,命中则执行规则。键名写法可能有:

  • [process.env.VUE_APP_BASE_API],这种中括号写法是因为变量作为键名必须这么写。当正式服 API 的路径有统一的根路径(假设为/server-api)时,即可采用这种写法。相应的,你需要在 .env.development 里设定:
ENV = 'development'
VUE_APP_BASE_API = '/server-api'
  • [process.env.VUE_APP_BASE_API + '/abc'],这种写法匹配/server-api/abc开头的所有路径。

  • '/abc',这种写法适用于没有统一根路径且以/abc开头的所有路径。

target

target是开发服(也可能是你的后端同事的笔记本)的 API 的域名。真正的请求会被发送到target的值 + API 相对路径组成的完整路径。例如:

例一:

设置:

键名target
[process.env.VUE_APP_BASE_API]http://www.dev.com

那么:

webpack-dev-server 的网址真正请求的网址
/server-api/abc/def?id=1http://www.dev.com/server-api/abc/def?id=1
/server-api/xyz?id=1http://www.dev.com/server-api/xyz?id=1

例二:

设置:

键名target
[process.env.VUE_APP_BASE_API + 'abc']http://www.dev.com

那么:

webpack-dev-server 的网址真正请求的网址
/server-api/abc/def?id=1http://www.dev.com/server-api/abc/def?id=1
/server-api/xyz?id=1不是以/abc开头,所以匹配不上

现在有一个问题,如果开发服务器的根路径与正式服务器不同,这是有可能的,比如是/或者/jack-pc,那就需要把/server-api替换成/或者/jack-pc。这就涉及到下一个属性pathRewrite

pathRewrite

如果开发服务器的根路径是/,则写成['^' + process.env.VUE_APP_BASE_API]: '',,意思是将/server-api替换成空串,这样的话:

真正请求的网址从:
http://www.dev.com/server-api/abc/def?id=1
变成:
http://www.dev.com/abc/def?id=1

如果开发服务器的根路径是/jack-pc,则写成['^' + process.env.VUE_APP_BASE_API]: '/jack-pc',,这样的话:

真正请求的网址从:
http://www.dev.com/server-api/abc/def?id=1
变成:
http://www.dev.com/jack-pc/abc/def?id=1

changeOrigin

changeOrigin: true,表示请求标头的Host字段不是真实的localhost,而是与target一样的值,这样欺骗开发服务器以为是同域,就不会涉及到跨域麻烦。

防范生产环境字体图标加载时乱码

只在生产环境可能出现字体图标乱码的前提下使用:

js
module.exports = {
  css: {
    loaderOptions: {
      sass: {
        sassOptions: { outputStyle: 'expanded' },
      },
    },
  },
};

为 Nginx 生成 .gz 文件

由于 Nginx 的动态压缩是对每个请求的响应先压缩再发回前端,这样造成服务器浪费 CPU,所以 Nginx 通常开启 http_gzip_static_module 模块,直接读取已经压缩好的静态文件,不再动态压缩。现在前端要做的是要配合 Nginx 提前生成 .gz 文件。

sh
yarn add -D compression-webpack-plugin
js
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  configureWebpack: {
    plugins: [new CompressionPlugin()],
  },
};

删除生产环境console.*

js
const TerserWebpackPlugin = require('terser-webpack-plugin');

module.exports = {
  chainWebpack(config) {
    config.when(process.env.NODE_ENV !== 'development', (config) => {
      config.plugin('Terser').use(
        new TerserWebpackPlugin({
          terserOptions: {
            compress: {
              drop_console: true,
            },
          },
        })
      );
    });
  },
};

向 index.html 传递数据

可以参考《Vue CLI 技术解决方案 - 公共 CDN》《Vue CLI 技术解决方案 - APP 检查最新版本》,本文只介绍最简代码。

vue.config.js 配置如下:

js
module.exports = {
  chainWebpack(config) {
    config.when(process.env.NODE_ENV !== 'development', (config) => {
      config.plugin('html').tap((args) => {
        args[0].aaa = 123;
        args[0].bbb = ['<span>甲乙</span>', '<span>丙丁</span>'];
        args[0].ccc = { xxx: 'yyy' };
        return args;
      });
    });
  },
};

index.html 如果数据插入到属性中:

html
<meta name="anything" content="<%= htmlWebpackPlugin.options.abc %>" />

打包后得到:

html
<meta name="anything" content="123" />

index.html 如果数据当做节点插入:

html
<div>
  <%= htmlWebpackPlugin.options.bbb.join('') %><%= htmlWebpackPlugin.options.ccc.xxx %>
</div>

打包后得到:

html
<div><span>甲乙</span><span>丙丁</span>yyy</div>

条件语句、判断环境的语句举例:

<% if (pro【混淆字符,请删除我】cess.env.NODE_ENV === 'development') { %>
  <%= htmlWebpackPlugin.options.bbb.join('') %>
<% } else { %>
  <%= htmlWebpackPlugin.options.ccc.xxx %>
<% } %>

splitChunks 技术

下方只是举例:

js
module.exports = {
  chainWebpack(config) {
    config.when(process.env.NODE_ENV !== 'development', (config) => {
      config.optimization.splitChunks({
        chunks: 'all',
        cacheGroups: {
          libs: {
            name: 'chunk-libs',
            test: /[\\/]node_modules[\\/]/,
            priority: 10,
            chunks: 'initial', // 表示只有被“立即”引用的包才会被分包
          },
          elementUI: {
            name: 'chunk-elementUI', // 将 Element UI 打包成单文件
            priority: 20, // 权重 20 应高于其他的权重
            test: /[\\/]node_modules[\\/]_?element-ui(.*)/,
          },
          commons: {
            name: 'chunk-commons',
            test: resolve('src/components'), // 打包共用组件
            minChunks: 3, // 只有被使用 3 次才打包
            priority: 5,
            reuseExistingChunk: true, // 复用已存在的包
          },
        },
      });
    });
  },
};

❌ 已废弃和避免使用的技术

Preload 技术

Vue CLI 4.5 和更早版本会使用 Preload 技术,也就是<link rel="preload" />,以便加快网页打开速度,你可以打开一些 Vue CLI 4.5 项目看看 index.html 的<head></head>部分。但是 Vue CLI 目前已经到达 5.0 以上版本,自 5.0 起,Vue CLI 已经放弃了 Preload 技术,你可以看看新项目的 index.html 的<head></head>部分。

放弃 Preload 技术的原因我猜测是<link rel="preload" />在 Vue 项目中并没有发挥出明显作用,因为 Preload 技术的本质是浏览器根据<link rel="preload" />声明,提前下载位于<body></body>尾部的 JS 文件,事实上这种提前下载没有意义,因为<div id="app"></div> DOM 加载时只是一个空节点,加载时间是微秒级,所以这种“提前下载”没有意义。

事实上,Preload 技术更多是应用在传统早期开发的页面中,<body></body>内含有大量默认内容,如果不使用 Preload 技术,那么在 DOM 加载完成之后、JS 文件下载完成之前的毫秒瞬间,页面可能会出现瞬间的错乱和抖动。

另外,如果你发现某些基于 Vue CLI 4.5 的整合框架在 vue.config.js 中使用config.plugin('preload')优化<link rel="preload" />代码,如果以 Vue CLI 5.0 的态度来看,既然 Preload 技术都已然被放弃,那么再对<link rel="preload" />代码进行任何优化都没有意义。

所以,旧项目对<link rel="preload" />代码的优化可以继续保留,但是新项目应果断放弃 Preload 技术。

Prefetch 技术

说完 Preload 技术,顺带说 Prefetch 技术。它的原理是根据<link rel="prefetch" />声明,预加载之后页面需要加载的资源,假设首页有 10 个下级页面,开发者设置提前获取了它们的资源,然而某用户只访问了其中 2 个,那么对于该用户来讲,剩下的 8 个页面的预请求资源就白白加载,造成流量浪费。现在的移动网络用户中,流量多的用户的带宽往往也高,他们不需要 Prefetch 技术提升访问速度,而流量少的用户缺流量,他们讨厌 Prefetch 技术浪费流量。

所以,开发者要么根据用户访问趋势统计,针对极热门页面的资源去设置<link rel="prefetch" />,要么果断放弃 Prefetch 技术。况且 Vue CLI 5.0 也放弃了 Prefetch 技术。

config.optimization.runtimeChunk('single')配置

某些基于 Vue CLI 4.5 的整合框架使用这个配置,让 app.xxxxxxxx.js 分离出一个 runtime.xxxxxxxx.js 文件,这样的话,修改异步路由组件的内容后重新 build,则 app.xxxxxxxx.js 不会有变化,只有 runtime.xxxxxxxx.js 文件和组件对应的 JS 文件变化,可以让老访客继续使用原 app.xxxxxxxx.js 的缓存,避免下载新的 app.xxxxxxxx.js,属于一种优化措施。

但是,在 Vue CLI 5.0 中,分离了 runtime.xxxxxxxx.js 之后,修改异步路由组件后重新打包,会让 app.xxxxxxxx.js 也发生变化,发生变化的内容是类似这样的代码:

js
component: () = >t = >n.e(893).then(function() {
    var e = [n(893)];
    t.apply(null, e)
}.bind(this))["catch"](n.oe)

其中893是被修改的组件的编号,术语叫module idmodule id在 Vue CLI 4.5 中是一个 4 位字符的编号且恒定不变,但是在 Vue CLI 5.0 中,只要组件内容有变化,则module id不是恒定的,既然 app.xxxxxxxx.js 内容有变化,那么文件名一定随着变化。

所以,对于 Vue CLI 5.0 项目,要么你找到让module id也恒定不变的办法,要么就果断放弃这个方案。

script-ext-html-webpack-plugin 插件

在 Vue CLI 4.5 项目中,这个插件是配合上面的config.optimization.runtimeChunk('single')配置来使用的,它本意是,由于 runtime.xxxxxxxx.js 体积很小,只有几 KB,让浏览器专门开线程下载它不值得,所以这个插件将 runtime.xxxxxxxx.js 文件写成内联代码,填充到 index.html 内部,最终总代码量不变,还减少了一个浏览器线程,因此提高了性能。

但是,在 Vue CLI 5.0 中,首先说,这个插件根本不支持 webpack 5,作者放弃了开发,然后说,既然config.optimization.runtimeChunk('single')配置已经无法达到目的,那么此插件也就没有必要使用。

所以,对于 Vue CLI 5.0 项目,果断放弃这个方案。

是否应使用压缩图片的插件

流行度较高的插件有image-webpack-loader,但是它是基于imagemin的各种插件的,比如:

  • mozjpeg — Compress JPEG images

  • optipng — Compress PNG images

  • pngquant — Compress PNG images

  • svgo — Compress SVG images

  • gifsicle — Compress GIF images

这几个插件的麻烦之处在于:

  1. 安装image-webpack-loader的时候会同时安装它们,安装它们的过程包含二进制安装,这一步经常报错。(在package.json中声明"reSolutionss": {"bin-wrapper": "npm:bin-wrapper-china"},可以解决这个问题。)

  2. 同是安装二进制程序,桌面软件能批量压缩图片的工具软件多得很,比如ACDSeeXnConvert,其中后者是免费的,它的“转换”功能默认就会压缩图片。你可以在发布新版本的时候转换一次图片就行了。注意不要转换 gif 文件。

  3. 每次打包都要执行压缩,浪费时间。

所以,有条件的话应使用 Object Storage Service,即 OSS,对象存储服务,没条件应使用XnConvert在上线前执行一次压缩即可。

杨亮的前端解决方案