浅析Vite前端构建工具

155 阅读23分钟

浅析Vite前端构建工具

本篇文章意在帮助初学者了解Vite的工作流程以及学习常见配置

Vite简介

Vite(法语意为快速)是vue官方发布的新型前端构建工具,用于快速构建前端项目,显著提升开发体验,其特点就是快!

!!!注意:Vite是构建工具,虽然具有打包功能,但其打包能力来自其他打包工具,如在开发环境下使用esbuild进行打包,而在生产环境下则使用rollup进行打包,vite更注重的是前端的自动化流程。开发好的代码,通过自动化流程转换成生产环境代码。

为什么选择Vite

随着前端的发展,我们开始需要构建越来越大型的应用,需要处理的JavaScript代码呈指数级增长。

而传统的打包工具在构建这种大型项目时就会遇到性能问题,如项目启动速度缓慢,热更新(HMR)迟钝,这极大的影响了开发者的开发体验。

以下是常见的打包构建工具:

  • Webpack
  • rollup
  • parcel
  • gulp

这些工具在大型项目中都存在缓慢的服务启动和缓慢的热更新问题,而Vite恰好对这两个问题提出了解决方案,所以Vite在启动项目时的速度相当之快

所以Vite的优势就是:

  • 开发启动速度快
  • 热更新(HMR)响应迅速
  • 构建性能高效
  • 配置简单且扩展性强

Vite的打包构建策略

这里我们将简单梳理Vite的在开发阶段、热更新阶段,以及在生产阶段时的构建思路。

先来看vite官方文档中的两张图

前一张图是传统构建工具的打包构建流程图,它们在启动项目时采用的策略是对项目代码和其依赖进行全量打包后再启动服务。

后一张图是Vite的打包构建流程,Vite采用的策略是先启动服务,之后按需加载所需要的代码文件。

  • 图一

  • 图二

传统打包构建工具的问题

从不同的打包策略中我们可以分析出,造成传统打包工具服务启动缓慢,热更新响应速度慢的原因:

  • 必须打包后才能启动服务。当项目越大,代码文件越多时,打包需要处理的模块数量就越多,启动等待时间就会成先行指数级增长,大型项目启动耗时几分钟很常见。
  • 依赖处理无缓存。一些第三方依赖(如Vue、React、Axios、Lodash)通常体积大且变动少,每次启动/修改都会重复处理,就会造成资源浪费,进一步延长了启动时间。
  • 模块依赖链重新构建。当某个文件被修改时,工具不仅要重新编译这个文件,还要重新处理所有依赖它的上游模块,导致热更新速度慢。

开发环境打包

一开始Vite将应用中的模块区分为了两类,依赖源码

  • 依赖:大多是在开发时不会变动的第三方库(如loadash、elementPlus等)。

  • 源码:通常是开发者编写的代码,时常会被编辑,需要转换的文件(如Vue、CSS、JSX等)

开发环境下,Vite会使用esbuild对依赖进行打包(即依赖预构建)。

对于源码Vite通常将其打包工作交给了浏览器而不是打包,只在浏览器请求源码时进行转换并按需提供源码。

esbuild是go语言编写的打包工具,比使用JavaScript编写的打包工具速度快10-100倍。

热更新(HMR)

前面我们知道了,如果仅仅修改一个文件就要重新构建整个模块是及其低效的,而且体积越大时,更新的速度也会越慢。

所以Vite热更新的策略是精准、高效地更新变化的模块,避免全量页面刷新,同时尽可能保留应用状态。

整个热更新阶段,vite经历了以下几个步骤

  1. 首先开发服务器会实时监控文件修改。
  2. 监测到文件修改时,会通过模块依赖图,找到受变更影响的模块链。
  3. 针对变化模块(如 JS、CSS、Vue 等)快速生成更新代码。
  4. 然后通过 WebSocket 将更新指令发送给客户端。
  5. 客户端在接收指令后,动态替换旧模块、执行新代码,仅更新受影响部分,保留应用状态,避免全量刷新。

生产环境打包

Vite 在生产环境的打包构建思路与开发环境截然不同,核心目标是生成优化后的、可直接部署的静态资源,兼顾性能(加载速度、运行效率)和浏览器兼容性。

  1. 基于 Rollup 进行打包构建

首先VIte使用功能更成熟的Rollup 作为底层打包工具(开发阶段用的是esbuild,因为它对代码压缩、Tree-shaking 等优化更高效)。

  1. 配置Vite后可以进行的优化(生产环境下)
    • 自动将代码拆分为多个 chunk:
      • 入口 chunk:应用的主入口代码。
      • 公共 chunk:提取多个模块共享的代码(如工具函数、重复依赖),避免重复加载。
      • 异步 chunk:对 import() 动态导入的模块单独打包,实现 “按需加载”(如路由懒加载),减少初始加载体积。
    • Tree-Shaking 消除冗余代码:
      • 利用 Rollup 的 Tree-Shaking 能力,删除未被引用的代码(如未使用的函数、变量),减小最终 bundle 体积。
      • 对第三方库,优先使用其 ESM 版本(通过 package.jsonmoduleexports 字段),确保 Tree-Shaking 有效。
    • 对 JS、CSS 代码进行压缩:
      • JS:使用 terser 压缩(默认开启),移除空格、缩短变量名、合并语句等。
      • CSS:使用 cssnano 压缩,移除冗余样式、合并规则等。
    • 支持通过配置来开启代码混淆,提高逆向工程难度。
    • 优化图片、字体等静态资源体积:
      • 小资源(如小于 4KB 的图片)自动转为 base64 编码嵌入代码,减少网络请求
      • 大资源生成带哈希值的文件名(如 logo.8f3b7a.png),配合长期缓存策略(Cache-Control),提升二次加载速度。
    • 兼容性处理:
      • 通过 @vitejs/plugin-legacy 插件:生成兼容 ES5 的代码(使用 babel 转换语法),并且自动注入 polyfill(如 core-js),处理新 API 兼容问题。
      • 通过 postcss + autoprefixer自动添加CSS前缀,从而适配不同浏览器内核。
  2. 构建流程
    • 解析配置:读取 vite.config.js 中的生产环境配置(如 build 选项)。
    • 依赖预构建复用:复用开发环境预构建的第三方依赖(或重新构建,确保一致性),避免重复处理。
    • 模块打包:通过 Rollup 递归解析项目依赖,应用代码分割、Tree-Shaking 等优化,生成多个 chunk。
    • 资源处理:处理静态资源、压缩代码、生成哈希文件名。
    • 生成产物:输出最终文件到 dist 目录,包含 HTML、JS、CSS、静态资源等,并自动处理资源间的引用路径(如 HTML 中引入带哈希的 JS/CSS)。

依赖预构建

这里我们将了解在生产阶段时,Vite如何对依赖进行预构建,以及做了哪些优化。

目的

  • 更好的兼容性
  • 更好的性能

这里我们先简单创建一个Vite项目,方便后续讲解。

创建一个示例项目

首先,打开或者新建一个空的项目文件夹,在其根目录下打开控制台并执行以下命令初始化package.json文件。

npm init -y // -y:自动在package.json中填入默认配置,无需手动配置
或
pnpm init // pnpm无需 -y,默认进行配置

然后安装Vite

npm i vite -D // -D, --save-dev的缩写,作用是将依赖包安装为 “开发环境依赖”
或
pnpm add vite -D

然后初始化项目结构:

video-test
├─ node_modules
├─ main.js
├─ index.html  // 项目打包入口文件
├─ package-lock.json // 使用pnpm命令时是pnpm-lock.yaml
├─ package.json

在index.html中引入main.js

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- type="moudle"作用是告诉浏览器按照 ESM 规则解析和执行代码 -->
    <script type="moudle" src="./main.js"></script>    
</body>
</html>

依赖打包

Vite在启动开发服务器时,会对依赖进行预构建:Vite会使用esbuild对第三方依赖对其进行打包。打包的过程中就会执行兼容性处理路径解析文件缓存优化浏览器缓存优化,然后对源码进行一系列处理后交给浏览器打包。

兼容性处理

在开发时,很多第三方库或项目代码可能使用 CommonJS(CJS)、UMD 等非 ESM 模块化语法,而浏览器原生 ESM 只能识别 import/export 语法,无法直接运行 CJS 模块(如 requiremodule.exports)。

例如lodash库,执行以下命令引入lodash库:

npm i lodash 
或
pnpm add lodash

打开node_modules\lodash\_apply.js,我们可以看到lodash使用了非ESM标准的导入导出方式

Vite的开发服务器将所有代码视为原生 ES 模块,所以必须要处理兼容性问题。Vite采用策略是当浏览器对某个模块发送请求时使用esbuild对其进行转换操作,不仅解决了兼容性问题,还避免了全量打包带来的性能消耗。

路径解析

1.无法识别的路径

浏览器并不知道要到node_modules目录下去找对应依赖,即对于非相对路径和绝对路径的导入是无法识别的。

import axios from 'axios' // 没有使用绝对路径和相对路径,浏览器不知道怎么获取

为了处理无法识别例如'axios'这种裸模块名,Vite会在依赖构建阶段对其进行路径映射为具体的文件路径:

  • 分析依赖的入口文件(通过 package.jsonmainmodule 字段),确定该模块名实际对应的文件(如 node_modules/axios/dist/axios.esm.js)。
  • 预构建后的依赖会放到.node_modules/.vite/.deps目录下,并生成一个映射表(deps_map),记录裸模块名与实际路径的对应关系。

然后在浏览器解析到import axios from 'axios'而对Vite开发服务器发送请求时,Vite在拦截到该请求后,根据依赖构建阶段时创建的映射表将其路径重写 为具体的预构建文件路径(如 /node_modules/.vite/deps/axios.js),并返回给浏览器,使得浏览器能正确加载模块。

2.相对路径和别名路径的处理

对于项目内的相对路径(如 import './utils'),Vite 会自动补全文件后缀(.js.ts 等)和目录索引文件(index.js),避免浏览器因路径不全导致的 404 错误。同时支持通过 vite.config.js 配置 resolve.alias 自定义路径别名(如 @/utils),进一步简化路径编写。

文件缓存

打包后的代码会放到node_modules/.vite文件下面进行文件缓存(同时转换为ESM标准),并且将嵌套引用的依赖打包为一个文件,以减少浏览器的http请求(例如lodash库作为第三方依赖会被打包成一个js文件),并在之后的开发服务启动时使用这个缓存直到第三方依赖发生修改才重新进行依赖预构建,减少了依赖打包构建次数。

  • node_modules/.vite 目录下的缓存文件会包含版本哈希标识(如 react.js?v=123abc),哈希值由依赖内容、版本号及 Vite 配置共同生成,确保依赖或配置变更时,缓存会自动失效并触发重新构建。
  • 缓存目录中还会生成 _metadata.json 文件,记录依赖的构建信息(如入口、依赖链),用于 Vite 启动时快速校验缓存是否有效,减少重复扫描开销。

浏览器缓存

同时对于已预构建的依赖请求使用 HTTP 头 max-age=31536000, immutable 进行强缓存,以提高开发期间页面重新加载的性能。同样如果依赖源没有发生改变,这些请求将永远不会再次访问开发服务器,而是直接从级存中读取。而对于源码。

源码交给浏览器打包

  • 源码(如业务 JS、TS、Vue 组件等)会被 Vite 实时转换为 ESM 格式(例如将 TS 转 JS、处理 CSS 模块),但不进行合并打包,而是保留模块间的引用关系(如 import './component')。
  • 浏览器加载这些 ESM 模块时,会根据 import 语句主动发起子请求,由 Vite 服务器实时返回对应模块的转换结果,本质是将 “打包合并” 的工作分散到浏览器的原生模块加载过程中,减少开发服务器的计算压力,实现 “按需编译”。

配置智能提示

在添加配置项的时候会给出配置项联想,如图所示在配置对象中敲ser就会出现提示框

官方给了三种智能提示的配置方法,推荐使用下面这种方式

import { defineConfig } from 'vite'

export default defineConfig({
	//...
})

准备配置文件

准备好以下几个文件

vite.prod.config.js

import { defineConfig } from 'vite'
// 生产环境配置
export default defineConfig({  
  build: {
      
  }
})

vite.dev.config.js

import { defineConfig } from 'vite'
//开发环境配置
export default defineConfig({ 
  server: { 
  
  }
})

vite.base.config.js

import { defineConfig } from 'vite'
// 公共配置
export default defineConfig({                        
})

将上诉的配置文件汇总,采用策略模式,在不同环境下引入不同的配置

vite.config.js

import { defineConfig } from "vite"
import viteBaseConfig from './vite.base.config.js' //基础配置
import viteDevConfig from './vite.dev.config.js' // 开发环境配置
import viteProdConfig from './vite.prod.config.js' // 生产环境配置

// 策略模式
const envResolver = {
  // 这里的展开运算符和Object.assign()都是浅拷贝的方法
  "build": () => ({...viteBaseConfig, ...viteProdConfig}),
  "serve": () => Object.assign({},viteBaseConfig, viteDevConfig)
}
export default defineConfig( ({command}) => {
  // command: 根据命令(pnpm dev | pnpm serve) 返回 ("build" | "serve")
  console.log("command:", command);

  return envResolver[command]()
})

环境变量(env)

环境变量:是在操作系统或应用程序运行时存在的一组动态键值对(key-value),用于存储影响程序行为的配置信息。它们独立于程序代码本身,通过外部传递给程序,使程序能在不同环境(如开发、测试、生产)中灵活调整行为,而无需修改代码。

loadEnv

是Vite的一个专门加载和处理环境变量的API,类似于dotenv库 dotenv 是一个非常实用的第三方库,主要用于从 .env 文件中加载环境变量到 process.env

loadEnv的三个参数

  • mode: 当前运行模式(如 development、production、test,注意拼写)
  • envDir: .env 相关文件所在目录(默认项目根目录)
  • prefix: 环境变量的过滤前缀(用于后续注入import.meta.env,默认VITE_)

loadEnv执行过程

  1. 根据mode加载指定目录下的环境文件,加载顺序(优先级从低到高):

    • .env (所有环境共享的基础配置)
    • .env.local (本地私有基础配置,覆盖.env,通常不上传仓库)
    • .env.${mode} (当前模式的专属配置,如.env.development)
    • .env.${mode}.local (当前模式的本地私有配置,覆盖.env.${mode},不上传仓库) 同时,系统环境变量的优先级最高,会覆盖所有文件中的同名变量
  2. 变量合并规则:后加载的配置会覆盖前序配置中的同名变量 例如mode="development"时,最终合并结果为:

    { ...基础.env, ...本地.env.local, ...开发环境.env.development, ...开发本地.env.development.local, ...系统环境变量 } // 展开运算符

  3. 函数返回值为合并后的所有环境变量(包含所有前缀/非前缀变量)

  4. loadEnv的返回值包含所有符合其自身prefix参数的环境变量(Node环境可见,用于配置逻辑) 但最终注入到import.meta.env的变量,由Vite的全局配置envPrefix(默认VITE_)决定:

    • 只有符合envPrefix前缀的变量,才会被注入到前端代码的import.meta.env中(浏览器环境可见);
    • envPrefix前缀的变量(即使被loadEnv返回),也不会暴露到前端,仅在Node环境的配置中可用(如后端密钥等敏感信息)。

手动加载机制

在配置 Vite 本身时(如自定义开发服务器端口、生产环境打包路径等),需要依赖环境变量动态调整配置。此时通过 loadEnv 手动加载 .env* 文件中的变量,可在配置逻辑中直接使用。

通常为了在大型项目中提高项目的可维护性,我们可以在不同环境下都维护一个环境变量文件,即创建以下三个文件:

  • .env:公共的环境变量文件
  • .env.development:开发环境下的环境变量文件
  • .env.production:生产环境下的环境变量文件

将这些配置文件放到项目根目录\config\env\

然后可以在vite.config.js中使用loadEnv方法统一获取,传递给不同环境下的配置文件

vite.config.js中修改:

import { defineConfig, loadEnv } from 'vite'
import viteDevConfig from './vite.dev.config.js'
import viteProdConfig from './vite.prod.config.js'
import viteBaseConfig from './vite.base.config.js'

// 策略模式
const endResolver = {
  // 添加一个参数
  "build": (env) => {
     console.log("生产环境");
     return Object.assign({}, viteBaseConfig, viteProdConfig(env))
  },
  // 添加一个参数
  "serve": (env) => {
     console.log("开发环境");
     return ({ ...viteBaseConfig, ...viteDevConfig(env)})
  } 
}

export default defineConfig(({command, mode}) => {
  // command: 根据命令(pnpm dev | pnpm serve) 返回 ("build" | "serve")
  // mode: vite会将命令(pnpm dev --mode delvelopment)中--mode后面的值development赋值给mode
  console.log("command:", command);
  console.log("mode:", mode);
  
  // 获取到对应环境下的环境变量,loadEnv的合并规则请查看执行过程中的描述
  const env = loadEnv(mode, './config/env', "")
  console.log(".env:", env);
  
  return  endResolver[command](env) // 将其作为参数传给对应的配置文件 
})

vite.prod.config.js

import { defineConfig } from 'vite'

export default (env) => defineConfig({
  server: {
    
  }
})

vite.dev.config.js

import { defineConfig } from 'vite'

export default (env) => defineConfig({
  build: {
    
  }
})

自动加载机制

Vite 会在项目构建或开发过程中,自动从指定目录加载环境变量文件,并将符合规则的变量注入到客户端代码的 import.meta.env。这一过程由 Vite 内部机制完成,无需开发者手动调用函数,核心依赖以下配置:

  • envDir 配置项:指定环境变量文件(如 .env.env.[mode])的存放目录(默认为项目根目录)。Vite 会根据当前运行模式(mode)自动从该目录加载对应的文件(如开发模式加载 .env.development)。
  • envPrefix 配置项:指定客户端可访问的环境变量前缀(默认为 VITE_)。只有带此前缀的变量会被注入到 import.meta.env,确保非必要变量不暴露给客户端。

所以我们需要在公共的配置文件中添加配置项

vite.base.config.js:

import { defineConfig } from 'vite'
import URL from ''
// 基础公共配置
export default defineConfig({
    envDir: './config/env' // 指定环境变量的加载目录
    envPrefix:'', // 前缀过滤
})

Vite对css的模块化处理

vite默认识别文件的方式

  • 新建index.css文件,在main.js中引入import 'index.css'
#app {
    background-color: pink;
    width: 200px;
    height: 200px;
}

效果:

我们发现了,在head标签里面新建了一个style标签,index.css 文件里里面的内容,都被写到了标签里面。

Vite 的开发服务器检测到 main.js 中导入了 index.css 时,会对该 CSS 文件进行特殊处理:它会将 CSS 内容转换为一个 JavaScript 模块,并在响应头中设置 Content-Type: text/javascript,浏览器就会认为这是一个 JS 脚本,从而让浏览器以js脚本的形式来加载后缀名为.css的文件。浏览器加载这个 JS 模块后,会执行其中的逻辑 —— 动态创建 <style> 标签,并将原本的 CSS 内容插入到该标签中,最终使样式生效。

转化为js脚本来加载CSS,主要有以下几个好处:

  • 用于热更新
  • 用于css模块化。
  • 通过ESM 原生加载和避免开发时打包,提高了编译性能。

CSS模块化

CSS 模块化(CSS Modules)是一种用于解决传统 CSS 的 “全局作用域” 问题,即当多个文件中出现同名类名时,后加载的样式会覆盖先加载的样式,导致样式冲突。

普通 CSS 文件(非模块化,如 style.css

类名会被原样保留,属于全局作用域,具体处理逻辑:

  1. 开发环境:Vite 将 CSS 内容通过动态创建的 <style> 标签注入到页面,类名直接生效(如 .title 会被浏览器解析为全局类名)。
  2. 生产环境:CSS 会被提取为独立文件,类名保持原样,加载后仍为全局作用域。

特点:多个文件中的同名类名会发生冲突(后加载的样式覆盖先加载的)。

CSS 模块化文件(如 style.module.css

类名会被哈希化处理,生成唯一标识,限制在局部作用域,具体流程:

Vite会对后缀名为.module.css进行模块化处理。

  1. 类名转换:Vite 会对模块化 CSS 中的类名进行哈希处理(如 .titletitle_1a2b3c),确保全局唯一,避免冲突。
    • 转换规则可通过 Vite 配置自定义(如 [name]__[local]___[hash],其中 name 是文件名,local 是原类名)。
  2. 在 JS 中引用:导入模块化 CSS 后,会得到一个对象,键为原类名,值为哈希后的类名。

创建a.js、b.js、 a.module.css、b.moudle.css四个文件分别放入如下内容

a.js

import aModuleCss from './a.module.css'
console.log("aModuleCss:",aModuleCss);

b.js

import bModuleCss from './b.module.css'
console.log("bModuleCss:",bModuleCss);

a.module.css

.demo {
    width: 100px;
    height: 100px;
    background-color: pink;
}

b.moudle.css

.demo {
    width: 100px;
    height: 100px;
    background-color: skyblue;
}

然后在main.js中引入 a.js和b.js文件,启动服务,查看浏览器控制台

然后我们可以看到,vite创建了两个映射对象,我们写的样式会被处理成key:value的形式,以原类名作为键名,处理后的类名作为键值。

image-20251113222925461转存失败,建议直接上传图片文件

然后真实引入的样式的类名会被替换成处理后的键值。最后再创建两个元素添加处理后的类名,写入如下代码

a.js

import aModuleCss from './a.module.css'
console.log("aModuleCss:",aModuleCss);

const div = document.createElement('div')
div.className = aModuleCss.demo //原类名demo作为属性名,处理后的类名作为属性值
document.body.appendChild(div)

b.js

import bModuleCss from './b.module.css'
console.log("bModuleCss:",bModuleCss);

const div = document.createElement('div')
div.className = bModuleCss.demo
document.body.appendChild(div)

效果:

渲染效果:在 HTML 中最终应用的是哈希后的类名(如 <div class="_demo_10cvw_1">),并且仅对当前模块生效。

总结:开启css模块化之后,除了会进行默认的css处理,还会进行类名替换,防止全局类名冲突

css.modules配置行为

在 Vite 中,css.modules 配置项用于自定义 CSS Modules 的行为(CSS Modules 是一种通过局部作用域类名避免样式冲突的技术)。Vite 内置了对 CSS Modules 的支持,而 css.modules 就是用来精细化控制其规则的,下面详细讲解每个配置的作用、使用场景和示例:

基础前提

在 Vite 中,默认约定:文件名以 .module.css(或 .module.scss 等预处理器后缀)结尾的文件会被当作 CSS Modules 处理,类名会被自动转换为局部作用域(编译成哈希字符串,如 header -> header_1a2b3c);非 .module 结尾的 CSS 文件则默认是全局作用域(类名不变)。

css.modules 完整配置项详解

以下是 vite.config.jscss.modules 的所有可选配置,结合 Vite 实际使用场景说明:

interface CSSModulesOptions {
  getJSON?: (cssFileName: string,
    json: Record<string, string>,
    outputFileName: string,
  ) => void
  scopeBehaviour?: 'global' | 'local'
  globalModulePaths?: RegExp[]
  exportGlobals?: boolean
  generateScopedName?:
    | string
    | ((name: string, filename: string, css: string) => string)
  hashPrefix?: string
  /**
   * default: undefined
   */
  localsConvention?:
    | 'camelCase'
    | 'camelCaseOnly'
    | 'dashes'
    | 'dashesOnly'
    | ((
        originalClassName: string,
        generatedClassName: string,
        inputFile: string,
      ) => string)
}
1. scopeBehaviour:默认作用域行为
  • 类型'global''local'(字符串,二选一)

  • 默认值'local'

  • 作用

    :指定 CSS Modules 文件中类名的默认作用域(全局 / 局部)。

    • 'local'(默认):所有类名默认是局部的(会被编译成哈希,避免冲突)。
    • 'global':所有类名默认是全局的(不编译,和普通 CSS 一样),此时需要用 :local() 手动指定局部类名。
  • 示例

    modules: {
      scopeBehaviour: 'global' // 默认全局,需手动用 :local() 定义局部类
    }
    

    对应 CSS(style.module.css):

    /* 全局类(因为 scopeBehaviour 是 global) */
    .title { color: red; }
    
    /* 局部类(手动用 :local() 标记,会被编译) */
    :local(.content) { font-size: 16px; }
    
2. globalModulePaths:指定全局 CSS 文件路径
  • 类型:正则表达式数组(如 [/\.global\.css$/]

  • 作用:通过正则匹配文件路径,符合规则的 .module.css 文件会被当作全局 CSS 处理(类名不编译),忽略 .module 后缀的约定。

  • 使用场景:需要批量将某些 .module.css 文件设为全局(比如第三方组件的样式)。

  • 示例

    modules: {
      globalModulePaths: [/node_modules/]// 匹配 node_modules 下的 .module.css 文件,当作全局处理
      // 另一个例子:匹配所有带 .global. 的文件,如 home.global.module.css
      // globalModulePaths: [/\.global\./]
    }
    
3. exportGlobals:是否导出全局类名
  • 类型:布尔值(true / false

  • 默认值false

  • 作用:如果 CSS 中用 :global() 定义了全局类名,是否将其导出到模块的 styles 对象中(方便在 JS 中引用)。

  • 示例

    modules: { exportGlobals: true }
    

    CSS(style.module.css):

    /* 全局类 */
    :global(.global-title) { color: blue; }
    
    /* 局部类 */
    .local-content { font-size: 14px; }
    

    JS 中引用:

    import styles from './style.module.css';
    console.log(styles); 
    // 因为 exportGlobals: true,会包含全局类:
    // { globalTitle: 'global-title', localContent: 'local-content_1a2b3c' }
    

    此时可以用 className={styles.globalTitle} 引用全局类。

4. generateScopedName:自定义局部类名格式
  • 类型:字符串 或 函数

  • 默认值:开发环境是 '[name]__[local]___[hash:base64:5]'(如 style__content___1a2b3),生产环境是短哈希(如 1a2b3

  • 作用:自定义局部类名的编译结果(默认是哈希,可读性差,可通过此配置优化)。

  • 字符串模式

    (常用占位符):

    • [name]:CSS 文件名(不含后缀)
    • [local]:原始类名(如 .content 中的 content
    • [hash:长度]:哈希值(可指定长度,如 [hash:6] 生成 6 位哈希)
    • [hash:base64:长度]:base64 格式的哈希
  • 函数模式:接收 3 个参数,返回自定义类名字符串:

    • name:原始类名
    • filename:CSS 文件路径
    • css:CSS 内容
  • 示例

    modules: {
      // 字符串格式:文件名__原始类名--6位哈希
      generateScopedName: '[name]__[local]--[hash:6]',
    
      // 函数格式:自定义逻辑(如添加前缀)
      generateScopedName: (name, filename, css) => {
        const prefix = filename.split('/').pop().replace('.module.css', '');
        return `my-${prefix}-${name}-${Math.random().toString(36).slice(2, 8)}`;
      }
    }
    

    假设文件是header.module.css,类名是.title,编译后可能是:

    • 字符串模式:header__title--1a2b3c
    • 函数模式:my-header-title-xyz789
5. hashPrefix:哈希前缀
  • 类型:字符串

  • 作用:给生成的哈希值添加一个前缀,确保不同项目 / 环境的类名哈希不冲突(哈希计算会包含这个前缀)。

  • 示例

    modules: { hashPrefix: 'my-project-v1' }
    

    此时哈希值会基于my-project-v1计算,避免和其他项目的哈希重复。

6. localsConvention:JS 中类名的命名格式
  • 类型:字符串('camelCase' / 'camelCaseOnly' / 'dashes' / 'dashesOnly')或 函数

  • 默认值undefined(不转换,保持原始类名)

  • 作用:CSS 中类名常用横杠(如 .user-info),但 JS 中更习惯驼峰(userInfo),此配置用于自动转换导出到 JS 的类名格式。

  • 字符串选项说明:

    • 'camelCase':横杠转驼峰,同时保留原始类名(如 .user-info 可通过 styles.userInfostyles['user-info'] 访问)。
    • 'camelCaseOnly':只保留驼峰形式(styles.userInfo 有效,styles['user-info'] 无效)。
    • 'dashes':驼峰转横杠,同时保留原始类名(如 .userInfo 可通过 styles['user-info']styles.userInfo 访问)。
    • 'dashesOnly':只保留横杠形式(styles['user-info'] 有效,styles.userInfo 无效)。
  • 函数模式:自定义转换规则,接收 3 个参数:

    • originalClassName:原始类名(如 user-info
    • generatedClassName:编译后的类名(如 user-info_1a2b3c
    • inputFile:CSS 文件路径
  • 示例

    modules: {
      localsConvention: 'camelCase' // 横杠转驼峰,保留原始
    }
    

    CSS(style.module.css):

    .user-info { color: green; }
    

    JS 中引用:

    import styles from './style.module.css';
    // 两种方式都能访问
    console.log(styles.userInfo); // 有效(驼峰)
    console.log(styles['user-info']); // 有效(原始)
    
7. getJSON:处理类名映射关系
  • 类型:函数

  • 作用:当 CSS Modules 编译完成后,会生成一个 “原始类名 -> 编译后类名” 的映射对象(JSON),此函数用于自定义处理这个映射(如保存到文件、打印日志等)。

  • 参数:

    • cssFileName:原始 CSS 文件的路径(如 src/style.module.css
    • json:映射对象(如 { "user-info": "user-info_1a2b3c" }
    • outputFileName:Vite 自动生成的映射文件路径(通常不需要手动处理)
  • 使用场景:需要记录类名映射关系(如调试、自定义生成类型文件等)。

  • 示例

    modules: {
      getJSON: (cssFileName, json, outputFileName) => {
        console.log('CSS 文件:', cssFileName);
        console.log('类名映射:', json);
        // 可以将 json 保存到本地文件(需要 fs 模块)
        // const fs = require('fs');
        // fs.writeFileSync('./class-map.json', JSON.stringify(json, null, 2));
      }
    }
    
实际项目常用配置示例

结合开发和生产环境的需求,一个典型的 css.modules 配置可能是这样的

// vite.config.js
export default {
  css: {
    modules: {
      // 开发环境下保留可读性好的类名,生产环境用短哈希
      generateScopedName: process.env.NODE_ENV === 'development'
        ? '[name]__[local]--[hash:5]'
        : '[hash:6]',
      // 横杠转驼峰,方便 JS 中用 . 访问
      localsConvention: 'camelCase',
      // 添加哈希前缀,避免冲突
      hashPrefix: 'my-app',
      // 导出全局类名,方便引用
      exportGlobals: true
    }
  }
}
总结

css.modules 配置的核心目的是:控制 CSS 类名的编译规则(局部 / 全局、格式)和 JS 中的引用方式。通过合理配置,可以让 CSS Modules 更符合项目的开发习惯,同时避免样式冲突。日常开发中,generateScopedName(自定义类名)和 localsConvention(驼峰转换)是最常用的两个配置。

css.preprocessorOptions(预处理器)

preprocessorOptions 就是给项目的 CSS 预处理器(比如 SCSS、Less)提供一个 “全局配置” 的地方。在 Vite 处理 .scss.sass 文件之前,它会先读取这些配置,并应用到整个编译过程中。

作用

  1. 引入全局样式文件:你可能有一个 variables.scss 文件,里面定义了所有的颜色、字体、间距等变量;还有一个 mixins.scss 文件,里面是一些可复用的样式片段。你不想在每一个 .scss 文件的开头都写一遍 @import './variables.scss';,这太繁琐了。
  2. 定义全局变量 / 混合宏:有些简单的变量或混合宏(mixin),你可能不想专门为它创建一个文件,希望能直接在所有 SCSS 文件中使用。
  3. 修改 SCSS 编译器的行为:比如,让 SCSS 允许开发者使用 @debug 指令在控制台打印调试信息,或者想改变它处理 URL 的方式。

全局配置项目中sass的行为

在vite项目中使用sass预处理器需要先安装相关依赖

pnpm add sass sass-loader -D

安装完成后,不需要做任何其他配置。Vite 会自动检测到这些依赖,并在导入 .scss.sass 文件时,自动使用它们进行编译。

**1. additionalData **

这个属性允许你在每一个被编译的 SCSS/Sass 文件的开头,自动注入一段 SCSS 代码。

场景一:自动引入全局样式文件

假设你的项目结构如下:

src/
├── assets/
│   └── styles/
│       ├── variables.scss  // 定义了 $primary-color: #1890ff;
│       └── mixins.scss     // 定义了 @mixin flex-center { ... }
└── components/
    └── Button/
        └── index.scss

如果你不配置 additionalData,那么在 Button/index.scss 中,你必须这样写:

// Button/index.scss
@import '../../assets/styles/variables.scss';
@import '../../assets/styles/mixins.scss';

.button {
  color: $primary-color;
  @include flex-center;
}

如果项目大了,每个文件都要写这么一长串 @import,不仅麻烦,而且容易出错。

使用 additionalData 后,你的 vite.config.js 可以这样配置:

// vite.config.js
import { defineConfig } from 'vite';
import path from 'path'; // 需要引入 path 模块来处理路径

export default defineConfig({
  // ... 其他配置
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `
          @import "@/assets/styles/variables.scss";
          @import "@/assets/styles/mixins.scss";
        `,
      },
    },
  },
  // 为了让 @/ 能正确指向 src 目录,通常需要配置 resolve
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
});

注意:

  • @/ 是一个别名,代表 src/ 目录,这需要在 resolve.alias 中配置。
  • 引入的文件路径是相对于项目根目录的,或者相对于 alias 配置的路径。
  • 可以把这段注入的代码想象成一个 “隐形的” 头部,自动加到了每个 .scss 文件的最前面。

现在,你的 Button/index.scss 就可以变得非常干净

// Button/index.scss
.button {
  color: $primary-color; // 直接使用,无需手动导入
  @include flex-center;  // 直接使用
}

场景二:定义全局变量或混合宏

如果你只有一两个简单的全局变量,不想为此创建一个文件,可以直接写在 additionalData 里。

// vite.config.js
export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData:  `
          $primary-color: #1890ff
          $secondary-color: #41b883
          $font-size-base: 16px
          $border-radius: 4px
        `,
      },
    },
  },
});

这样,在任何 .scss 文件中,你都可以直接使用 background: $primary-color;

2. importer (高级用法)

这是一个函数,用于自定义 SCSS 的 @import 解析逻辑。当 SCSS 编译器遇到一个 @import 语句时,它会调用这个函数,让你有机会去自定义文件的查找和读取方式。

这通常用于一些比较复杂的场景,例如:

  • node_modules 中引入文件时,可以省略长长的路径。
  • 根据不同的环境(开发 / 生产)引入不同的主题文件。

示例:

假设你想让 @import 'my-theme'; 能够直接找到 src/themes/main.scss 文件。

// vite.config.js
import { defineConfig } from 'vite';
import path from 'path';
import fs from 'fs'; // 文件系统模块

export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        importer(url, prev, done) {
          // url 是 @import 的目标,比如 'my-theme'
          // prev 是当前文件的路径
          
          if (url === 'my-theme') {
            const themePath = path.resolve(__dirname, 'src/themes/main.scss');
            // 读取文件内容并返回
            return { contents: fs.readFileSync(themePath, 'utf8') };
          }
          
          // 如果不是我们关心的 url,就返回 null,让 SCSS 按默认方式处理
          done({ file: url });
        }
      },
    },
  },
});
3. outputStyle

这个配置项用来指定最终生成的 CSS 代码的风格。

  • 'expanded': 展开的风格,每个选择器和属性都单独一行,可读性最好(默认)。
  • 'compressed': 压缩后的风格,所有代码都挤在一行,没有空格和换行,文件体积最小。

注意:这个配置是作用于 SCSS 编译器本身的。在 Vite 中,开发环境下,最终的 CSS 输出还会受到 Vite 自身的 build.cssMinify 配置影响。通常,我们只在需要单独调试 SCSS 输出时才会关注这个选项。

css.devSourcemap(源映射)

devSourcemap 是开发环境中用于生成 CSS 源映射的配置,让浏览器开发者工具直接关联原始源码(如 SCSS、Less),而非编译后的压缩 CSS。

因为使用 CSS 预处理器(SCSS、Less、Stylus)时,编译后的代码与源码结构差异大,需源映射关联,以提高调试效率。

// vite.config.js
export default {
  css: {
    devSourcemap: true, // 开发环境开启源映射
    preprocessorOptions: {
      scss: { /* 预处理器配置 */ }
    }
  }
}

当我们不配置devSourcemap

// vite.base.config.js
import { defineConfig } from 'vite'
// 基础公共配置
export default defineConfig({
    css: { // 配置css行为
        devSourcemap: false
    }
})

启动开发服务器,如图当我们查看某一个元素的样式时,不会指出css来源

当配置devSourcemaptrue时,再次查看

推荐在开发环境下开始源码映射,生产环境建议关闭,避免暴露源码结构.

css.postcss

这是 Vite 中用于配置 PostCSS 处理器的核心选项。PostCSS 本身是一个用 JavaScript 工具和插件转换 CSS 代码的工具,可以通过其丰富的插件来实现自动添加浏览器前缀、CSS语法降级、代码质量检查与规范约束、使用未来 CSS 语法、代码压缩优化等功能。在 Vite 中内置并支持PostCSS,但是如果要使用它的插件需要安装相关依赖

常用的PostCSS插件

  • postcss-preset-envPostCSS预设包,预设了一些常用的插件的配置项(例如autoprefixer的自动补全)

  • autoprefixer: 根据package.jsonbrowserslist配置自动添加浏览器前缀(如-webkit--moz-

  • cssnano:压缩 CSS 代码,移除空格、注释、合并重复规则,减少文件体积

  • stylelint:检查 CSS 代码中的语法错误和风格问题(如属性排序、单位规范)

  • postcss-modules:实现CSS模块化,避免样式污染(类似Vue的<style scoped>)

**browserslist 配置 **

package.json中来配置兼容范围的好处就是,可以使得babelPostCSS处理语法兼容性的问题时的兼容范围一致

// package.json
{
	"browserslist": ["cover: 99.5%"]
//"cover: 99.5%" 表示让 PostCSS 等工具适配全球 99.5% 的浏览器,
}

vite中配置

// vite.config.js
import { defineConfig } from 'vite'
import postcssPresetEnv from 'postcss-preset-env'
// import autoprefixer from 'autoprefixer'
export default defineConfig({
    css: {
       postcss: { // 配置postcss,完全支持在.postcss.config.js中的所能使用的配置项
            plugins: [ // 安装postcss插件
                postcssPresetEnv({})
                // autoprefixer({
                //     // 指定兼容 99.5%的浏览器。
                //     // browsers: ["cover 99.5%"],
                //     // 也可以在package.json中配置browerslist配置项
                // })
            ]
        },
    }
})

b.modules.css中:

.demo {
    width: 100px;
    height: 100px;
    background-color: skyblue;
}

.demo-content {
    width: 100px;
    height: 100px;
    background-color: green;
    display: flex; // 添加该行属性
}

Vite加载静态资源

静态资源

静态资源通常来说是网站中哪些不会随请求变化而变化的文件,浏览器可直接下载并渲染 / 使用,无需额外编译(或仅需前端构建工具预处理)。

常见的静态资源包括:

  • 图片:PNG、JPG、GIF、SVG、WebP等
  • 字体:TTF、OTF、WOFF、WOFF等
  • 媒体:MP4、WebM、MP3、AAC等
  • 样式:CSS文件
  • 脚本:不涉及服务端动态逻辑的JS文件(第三方库、前端工具脚本)
  • 其他:图标(favicon.ico)、JSON配置文件、静态HTML等

vite中的静态资源

Vite 中对静态资源处理的范围是静态资源的一个子集,主要识别一下两种为Vite所认为的静态资源

  • 当你在代码中通过 importrequire 导入一个非 JavaScript 文件时,Vite 会将其视为一个需要被处理的静态资源。
// main.js
import logo from './assets/logo.png'; // Vite 会处理 logo.png
import style from './style.css';     // Vite 会处理 style.css
  • Vite 会将项目根目录下 public 文件夹中的所有文件,视为无需处理的静态资源

生产环境下Vite对静态资源的处理行为

浏览器缓存

我们通过之前的学习已经知道了浏览器的缓存机制(尤其是强缓存,如 Cache-Control: max-age=31536000)会根据文件名来判断是否需要重新请求资源。所以呢当文件内容变动而文件名没有发生改变时,浏览器会继续使用缓存文件,而不是重新请求文件。

Vite 的hash命名处理

Vite 根据每一个文件的内容计算出一个哈希值,然后将这个哈希值作为文件的一部分对文件进行重命名(demo.[hash].js)

,所以一旦内容发生变动,哈希值就会发生变动。

与浏览器缓存机制的配合

利用浏览器缓存策略和Vite的哈希计算命名,就可以很好的控制浏览器的请求资源的行为:

  • 当文件没有发生改变时,文件名不变,浏览器使用缓存文件。
  • 当文件内容发生改变时,文件名发生改变,浏览器重新请求文件,并再次缓存。

例如在main.js的内容如下

console.log('hello world')

启动生产构建命令:pnpm build时,查看项目根目录下的dist目录下

当在最后一行添加注释后,重新构建

console.log('hello world')
// 新增注释

可以看到文件名发生了变化

.build.rollupOptions

在该配置项下可以配置Rollup的打包行为,因为生产环境下 Vite 使用 Rollup进行打包。

import { defineConfig } from 'vite'
// 生产环境配置
export default (env) => defineConfig({
    build: {
        assetsInlineLimit: 10 * 1024, 
        rollupOptions: { // 配置rollup的一些构建策略
            output: { // 控制输出行为
                assetFileNames: "[hash].[name].[Sext]"
                /** 处理打包后静态资源文件名
                 * * hash - Rollup根据文件内容计算的哈希值
                 * * name - 原文件名,如demo.js,name="demo"
                 * * .ext - 文件后缀名,如`.js、.vue...`
                 */
            },
        },
    },
})

运行一下pnpm build,查看dist目录下的变化,可以看到静态资源的文件名处理行为已经改为hash值在前了

.build.assetsInlineLimit

  • 默认值4096 (4KB)。
  • 生产环境下,对于小于或等于 assetsInlineLimit 的值,Vite 会将其直接内联到最终的 JS 或 CSS 文件中,而不是生成一个单独的文件。
import { defineConfig } from 'vite'

// 生产环境配置
export default (env) => defineConfig({
    build: {
        assetsInlineLimit: 10 * 1024,
    }
})

如果某个图片资源小于10KB,Vite就会将其转换为base64格式内联到项目中,反之则处理为静态资源。

对于有很多图标或者图片小于10KB的项目来说可以极大的减少http请求,提高网页性能。不过转换为base64字符串格式之后,体积一般会增大30%。

.build.outDir

  • 配置生产环境打包后的文件夹名
  • 默认值:"dist"
  • **类型:**字符串

.build.assetsDir

  • 配置生产环境打包后的静态资源文件夹名
  • 默认值:"assets"
  • **类型:**字符串

配置路径别名

通常在项目中问我们可以看到如下的导入方式

import SchedulStaff from '@/views/schedule/staff/index.vue'

可以看到路径开头是'@'不是相对路径也不是绝对路径的形式,这就是路径别名,用@来代替部分路径。

所以对于深层目录下的文件导入上层资源时,容易出现 ../../views/schedule/staff/index.vue 这类难以维护的路径,@ 可直接简化为 @/views/schedule/staff/index.vue

.resovle.alias

import { defineConfig } from 'vite'
import path from 'path' // node内置模块

export default defineConfig({
	resolve: {
		alias: {
			"@": path.resolve(__dirname, 'src'),
            "@assets": path.resolve(__diname, 'src/assets'),
            /**
             * path.resolve() - path模块提供的拼接路径的方法
             * ——dirname - node内置变量,获取当前配置文件所在目录的绝对路径
             */
		}
	}
})

配置完成后,在Vite项目中就可以使用@来代替指定的路径。

src/test.js中写入

console.log('路径别名配置成功')

main.js中引入

import '@/test.js'

启动项目,打开浏览器控制台:

路径提示以及TS项目中配置

默认情况下配置好了路径别名后,Vite能识别别名但是不会做出路径提示

可以在jsconfig.json或者tsconfig.json中写入如下配置

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"], // 与 Vite 配置一致
      "@assets/*": ["src/assets/*"]
    }
  }
}

以上配置有如下效果

  • 在项目中出现路径提示功能
  • TypeScript 项目,vite中配置别名后需要配合在 tsconfig.json 中补充路径映射,否则 TS 会提示 “模块找不到”:

两种路径风格

在用 Vue 脚手架工具时我们会发现,在配置路径别名的时候,路径配置用的时如下方式,fileURLToPath(new URL('src', import.meta.url))这种方式是现代的 ES Modules (ESM) 风格,而对于path.resolve(__dirname, 'src')CommonJS风格的方式。

  • import.meta.url: 这是 ESM 模块中的一个元数据属性。它返回当前模块文件的文件 URL 地址
  • new URL('src', import.meta.url): 这里使用了浏览器和 Node.js 都支持的 URL 构造函数。它的第二个参数是基准 URL。这个操作会将相对路径 'src' 解析为基于当前模块 URL 的一个绝对 URL。结果会是 file:///path/to/your/project/src
  • fileURLToPath(): 这是 Node.js url 模块提供的一个方法。它的作用是将一个 file: 协议的 URL 对象或字符串,转换为 Node.js 所使用的本地文件系统路径字符串(例如 C:\path\to\src/path/to/src)。
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
	resolve: {
		alias: {
            "@assets": fileURLToPath(new URL('src', import.meta.url)),
		}
	}
})

Vite使用的是ESM风格,所以 Vite也更推荐使用fileURLToPath(new URL('src', import.meta.url))

这种方式来进行路径配置。

Vite 插件

Vite 插件是基于 Rollup 插件接口 扩展的工具,用于增强 Vite 的核心能力(如模块转换、依赖处理、服务器中间件、构建优化等)。Vite 本身的很多核心功能(如 CSS 处理、TS 转译)也是通过内置插件实现的,第三方插件可以无缝集成,覆盖开发 / 构建全流程。

@vitejs/plugin-legacy

Vite 官方插件,核心作用是对 js 语法降级让生产环境的构建产物兼容旧版浏览器(比如 IE11、旧版 Chrome/Firefox)。

安装

// npm
npm install @vitejs/plugin-legacy --save-dev
// pnpm
pnpm add @vitejs/plugin-legacy -D

配置

import { defineConfig } from 'vite';
import legacy from '@vitejs/plugin-legacy';

export default defineConfig({
  plugins: [
    legacy({
      targets: ['defaults', 'not IE 11'] // 支持的目标浏览器(排除 IE11)
      // 也可以在package.json中配置browerslist配置项,统一css、js语法降级范围  
    })
  ]
});

使用:

配置完成后运行pnpm build命令Vite就会在对应的时机使用该插件进行JS语法降级

vite-plugin-mock

vite-plugin-mock 是一个在 Vite 项目中模拟后端 API 的插件,其内部依赖mock.js,它的核心作用是在开发环境中拦截前端发起的 API 请求,并返回预设的模拟数据,从而让前端开发可以不依赖真实的后端服务就能独立进行。

安装

npm install vite-plugin-mock --save-dev
pnpm add vite-plugin-mock -D

配置

import { defineConfig } from 'vite';
import { viteMockServe } from 'vite-plugin-mock'

export default defineConfig({
  plugins: [
		viteMockServe({
            mockPath: './src/mock', // 模拟数据文件所在的目录
            localEnabled: true, // 开发环境是否启用 mock
            prodEnabled: false, // 生产环境是否启用 mock(一般关闭)
        })
  ]
});

使用

在src目录下创建src/mock/user.js

// user.js
export default [
  {
    url: '/api/users', // 要拦截的 API 路径
    method: 'get', // 请求方法
    response: () => {
      // 模拟返回的数据
      return {
        code: 200,
        message: 'success',
        data: [
          { id: 1, name: '张三' },
          { id: 2, name: '李四' },
        ],
      };
    },
  },
];

在main.js中请求

// main.js
fetch('/api/users').then(res => {
    console.log(res);
})

启动开发服务,查看浏览器请求预览

生产环境性能优化

分包策略:代码分割

对于vite中的代码分割的原理可以参考这一篇文章:深入解析 Vite 代码分割原理:从依赖入口点算法到动态导入优化 - WangHaoyu的文章 - 知乎。

作用:

在实际项目中,我们在生产环境打包时,对于那些第三方库和自己的代码可以分开打包,这样做的好处有以下几点:

  • 消除代码冗余,减小总打包体积

大型项目中,多个页面可能共用同一个组件(如弹窗、按钮),不分割的话,这个组件会被重复打包到每个页面的 bundle 里,导致总体积膨胀。

代码分割会把 “公共代码” 抽成独立 chunk,只打包一次,所有页面共享 ——总打包体积更小,且不会出现重复代码

  • 减少首屏加载时间,提升用户体验

不分割的代码会打包成一个巨大的bundle.js(可能几 MB 甚至十几 MB),用户打开页面时,必须等这个文件完全下载、解析、执行才能看到内容,体验极差(尤其是弱网 / 移动端)。

代码分割后,会优先加载「首屏必需的代码」(比如首页渲染、登录逻辑),非必需的代码(比如详情页、设置页、大型组件)延迟加载(按需加载)。

  • 利用浏览器缓存,减少用户重复下载

把稳定不变的代码(第三方依赖、公共组件)拆成独立 chunk(比如vendors~main.js),这些 chunk 的文件名通常会带哈希(如vendors~main.123abc.js)。

用户第一次访问时下载这些 chunk,之后只要依赖 / 公共组件不更新,哈希值不变,浏览器会直接用缓存的文件,不用重新下载。 后续你修改业务代码时,只有 “业务 chunk” 的哈希变化,用户只需下载这个小体积的更新文件,而非整个大 bundle——更新迭代时的用户下载成本大幅降低

build.rollupOptions.output.manualChunks
build: {
    rollupOptions: {
        output: {
            // 第一种接收一个对象
            // manualChunks: {
            // // rollup会将node_modules下的vue、axios...等数组中有的库打包到vendor开头的文件中
            //     vendor: ['vue', 'axios', ...],
            // }
            // 第二种接收一个函数
            manualChunks: id => {
                // 将node_modules下的文件全部打包到vendor开头的文件中
                if (id.includes('node_modules')) {
                    return 'vendor'
                }
            }
        }
    }
}
示例:

例如,没有配置前运行pnpm build命令,查看控制台输出

可以看到所有代码(包括第三方库、自己写的代码)都被打包为了index-Bhk5f-Te.js,在main.js中添加一行代码,如console.log(111),重新运行pnpm build

可以看到我们仅仅修改了一小段代码,而在生产打包时,却是将所有代码都打包了,重新生成了新的hash值,这样每次修改代码,都要全量打包是非常消耗性能的,一是没法利用浏览器缓存机制来减少请求体积,二是每次更新的体积较大(因为不是只更新代码修改部分)。

下面我们来看看添加了配置项的情况:

manualChunks: id => {
    if ( id.includes('node_modules')) {
        return 'vendor'
    }
}

运行pnpm build命令,查看控制台:

可以看到原先的代码文件被打包成了两个,并且node_modules下的文件被打包为了vendor-开头的文件。然后我们再次修改main.js中的代码,重新打包得到如下结果: 这次打包我们可以观察到,Vite在打包时仅仅对index-开头的文件进行了重新打包,对于vendor-开头的文件而是利用缓存,不进行重新打包,并且我们可以看到,重新打包的那部分代码体积是远小于没有变动的那部分体积的。

gzip压缩

引言:

在 Vite 中,gzip 压缩是前端性能优化的关键手段之一——核心作用是减小静态资源(JS/CSS/ 图片等)的传输体积,从而提升首屏加载速度、降低服务器带宽成本,尤其对大体积 JS bundle 或弱网环境效果显著。

原理:

gzip 是一种无损数据压缩算法(压缩后能完全还原原始内容),其核心逻辑是:

  1. 识别文件中重复出现的字符串 / 数据,用「索引 + 标记」替代重复内容(比如 JS 中大量重复的变量名、函数体、注释);
  2. 压缩后的文件后缀通常为 .gz(如 main.js.gzindex.css.gz);
  3. 浏览器请求资源时,会在 HTTP 请求头中携带 Accept-Encoding: gzip, deflate(表示支持 gzip 压缩);
  4. 服务器收到请求后,若存在对应的 .gz 压缩文件,会返回压缩文件,并在响应头中添加 Content-Encoding: gzip
  5. 浏览器收到压缩文件后,自动解压并执行 / 渲染。
vite-plugin-compression2

Viteb本身没有内置gzip压缩的功能,所以需要借助vite-plugin-compression2这个插件来实现

pnpm add vite-plugin-compression2 -D

基本使用方式:

import { defineConfig } from 'vite'
import { compression } from 'vite-plugin-compression2'

export default defineConfig({
  plugins: [
    // ...其他插件
    compression()
  ]
})

常见配置项

// vite.config.js
import { defineConfig } from 'vite';
import compression from 'vite-plugin-compression2';

export default defineConfig({
  plugins: [
    // 1. gzip 压缩(兼容老浏览器,优先级低)
    compression({
      algorithm: 'gzip', // 压缩算法
      ext: '.gz', // 压缩文件后缀
      threshold: 1024, // 1KB 以上才压缩(小文件没必要)
      deleteOriginFile: false, // 不删除原始文件(兼容不支持压缩的浏览器)
      include: /\.(js|css|html|svg|json|txt)$/, // 只压缩文本类资源
      exclude: /\.(png|jpg|jpeg|webp|mp4|woff2)$/, // 排除已压缩的资源(图片/视频/字体)
      compressionOptions: { level: 6 }, // 压缩等级(0-9,6 是平衡值)
      filename: '[path][base].gz', // 压缩文件命名格式(默认即可)
    }),
    // 2. brotli 压缩(现代浏览器优先,压缩率更高)
    compression({
      algorithm: 'brotliCompress',
      ext: '.br',
      threshold: 1024,
      deleteOriginFile: false,
      include: /\.(js|css|html|svg|json|txt)$/,
      exclude: /\.(png|jpg|jpeg|webp|mp4|woff2)$/,
      compressionOptions: { level: 11 }, // brotli 最高等级 11,压缩率最优
    }),
  ],
});

使用注意

  1. 服务器 / CDN 需配置:支持根据 Accept-Encoding 请求头,返回对应的 .gz/.br 压缩文件(否则压缩产物白生成),并且如果CDN支持自动压缩,可以关闭压缩插件;
  2. 避免对无效压缩(如对png、jpg、woff等非文本类文件进行压缩,配置exclude避开),否则会浪费构建时间和存储空间。
  3. 小体积不要使用gzip压缩,因为浏览器解压也会消耗时间,所以对于小体积文件要避免进行压缩。
  4. 浏览器兼容:gzip 适配所有浏览器(含老版),brotli 覆盖 95%+ 现代浏览器,zstd 仅适合新版浏览器;
  5. 构建正常:vite build 能生成完整 dist 目录(无语法 / 依赖错误)。
示例:

正常运行构建命令pnpm build,我们其实可以看到控制台中的输出信息中给出了压缩前后的对比

红色部分是压缩前的体积,蓝色部分呢是采用gzip压缩后的体积。安装插件并使用:

可以看到压缩后的结果。

CDN优化

CDN 全称 Content Delivery Network,即内容分发网络,是一种经策略性部署的分布式网络架构,核心是通过在全球或区域内广泛部署边缘服务器节点,将源站的内容缓存到离用户最近的节点,让用户就近获取资源,以此解决网络带宽小、用户分布不均等导致的访问慢问题。

引入

在项目开发中,我们常会引入 Vue、axios、Lodash 等第三方库。默认打包流程中,这些库会被合并到生产环境的业务代码 bundle 中,最终由用户从业务服务器加载完整资源包。

假设有这样的场景:我们的业务服务器 B 部署在中国成都,而用户 A 位于美国纽约。当用户 A 访问网站时,所有资源(包括第三方库代码)都需要从成都的服务器 B 跨洋传输。由于地理距离遥远,网络链路长、延迟高,资源加载耗时久,可能出现页面白屏、交互卡顿等问题,严重影响用户体验。

这正是 CDN(内容分发网络)的核心优化场景 —— 我们无需自行在纽约部署服务器,而是可以借助成熟的公共 CDN 服务:将 Vue、axios 等通用性强的第三方库,托管到全球分布式的 CDN 节点(包括纽约附近的边缘节点)。当用户 A 访问网站时,会通过 CDN 的智能路由机制,自动从距离最近的纽约边缘节点加载这些第三方库资源,无需跨洋请求成都的服务器 B。

这样一来,第三方库的加载距离大幅缩短,网络延迟和传输耗时显著降低,配合业务代码的合理优化(如代码分割、本地缓存),用户 A 能更快地完成页面资源加载与渲染,网站访问速度和体验得到明显提升。

vite-plugin-cdn2
 pnpm add vite-plugin-cdn2 -D

配置示例

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import cdn2 from 'vite-plugin-cdn2';

export default defineConfig({
  plugins: [
    vue(),
    cdn2({
      // 新增全局配置(统一控制启用状态、CDN 源等)
      options: {
        enabled: process.env.NODE_ENV === 'production',
        defaultCDN: 'jsdelivr',
        autoComplete: true, // 自动补全 path 和 var,无需手动写
      },
      modules: [
        { name: 'vue' }, // 仅需写 name,其余自动识别
        { name: 'axios' },
      ],
    }),
  ]
})

跨域

产生跨域的原因是由于浏览器的同源策略,不符合同源策略条件就会产生跨域。

同源策略

协议、域名(含子域名)、端口三者必须完全一致,只要有一个不同,就是跨域:

对比项示例(当前页面:https://a.com:443是否跨域?
协议不同http://a.com(http vs https)
域名不同https://b.com(a vs b)
子域名不同https://api.a.com(主域 vs 子域)
端口不同https://a.com:8080(443 vs 8080)
仅路径不同https://a.com/user(路径不同)否(同源)
仅查询参数不同https://a.com?name=test(参数不同)否(同源)

⚠️ 注意:「跨域」仅存在于浏览器端(浏览器的安全限制),服务端之间的请求(如 Node.js 调用接口、Nginx 反向代理)完全没有跨域问题 —— 这也是 代理方案 能解决跨域的核心原理。

例如

直接请求百度

axios.get('http://www.baidu.com').then(res => {
    console.log(res)
})

浏览器报错

开发环境:Proxy 代理

通过「中间服务器转发请求」,绕开浏览器同源策略。

1. 核心配置(vite.config.js)

假设前端项目地址:http://localhost:5173(Vite 默认端口),后端接口地址:https://api.b.com(跨域),配置如下:

export default defineConfig({
  server: {
    proxy: {
      // 1. 基础代理:匹配所有以 /api 开头的请求
      '/api': {
        target: 'https://www.baidu.com', // 后端接口根地址
        changeOrigin: true, // 关键:让后端认为请求来自代理服务器(而非浏览器),避免后端校验 Origin 失败
        rewrite: (path) => path.replace(/^\/api/, ''), // 可选:重写路径(如前端 /api/user → 后端 /user)
        // 可选:处理 HTTPS 接口(若后端是 HTTPS,需配置)
        secure: false, // 忽略 SSL 证书校验(开发环境可用,生产环境需开启 true)
        headers: {
          // 可选:添加自定义请求头(如授权、版本号)
          'X-Proxy': 'Vite-Proxy',
        },
      },
    },
  },
});
2. 前端请求示例
axios.get('/api').then(res => {
    console.log(res)
})

请求成功并且控制台没有报错信息:

3. 注意
  • 「后端报 Origin 不允许」:必开 changeOrigin: true(让代理服务器转发时,将 Origin 改为 target 地址,而非前端 localhost:5173);
  • 「HTTPS 接口请求失败」:开发环境配置 secure: false(忽略证书校验),生产环境需确保后端 SSL 证书有效,再改为 true
  • 「路径匹配错误」:若后端接口没有统一前缀(如直接是 https://api.b.com/user),前端无需加 /api,直接匹配 '/'(但不推荐,可能冲突);
  • 「代理不生效」:检查 Vite 是否重启(修改 proxy 后需重启 Vite),或请求路径是否匹配代理规则(如 /api/user 才会触发 /api 代理)。

生产环境:2 种核心方案(Nginx 代理 vs CORS 跨域头)

生产环境没有 Vite 代理,需通过服务端代理CORS 跨域头解决,两者各有适用场景:

方案 1:Nginx 反向代理(推荐,适合前后端分离部署)

原理和 Vite Proxy 一致:通过 Nginx 转发前端请求到后端,绕开浏览器跨域限制 ——优势是前端无需任何修改,跨域逻辑完全由服务端管控

  • 前后端分离部署(前端 Nginx 托管,后端独立部署);
  • 后端不允许直接对外暴露(需通过 Nginx 做权限控制、限流);
  • 需隐藏后端真实地址(提高安全性)。
方案 2:后端配置 CORS 跨域头(简单直接,适合后端可修改)

你的理解很准:通过后端返回 Access-Control-Allow-* 系列头,告诉浏览器「允许该源的跨域请求」——优势是无需配置 Nginx,后端单独搞定

核心 CORS 响应头(后端需返回)

响应头作用说明示例值
Access-Control-Allow-Origin允许跨域的源(必填):* 表示所有源(不推荐,有安全风险),指定域名更安全https://a.com(仅允许 a.com
Access-Control-Allow-Methods允许的请求方法(必填)GET,POST,PUT,DELETE,OPTIONS
Access-Control-Allow-Headers允许的自定义请求头(如 Token、Content-Type)Authorization,Content-Type
Access-Control-Allow-Credentials是否允许携带 Cookie(可选):true 表示允许(此时 Origin 不能为 *)true
Access-Control-Max-Age预检请求(OPTIONS)的缓存时间(可选):减少 OPTIONS 请求次数86400(24 小时)

前端请求示例(需携带 Cookie 时)

axios.get('https://api.b.com/user', {
  withCredentials: true, // 关键:允许携带 Cookie(需和后端 Access-Control-Allow-Credentials: true 配合)
});

适用场景:

  • 后端可直接对外暴露(如公开 API);
  • 前后端部署在不同域名,且不想配置 Nginx;
  • 需携带 Cookie 进行身份验证(如单点登录)。

安全注意:

  • 禁止用 Access-Control-Allow-Origin: *(允许所有源跨域,有 CSRF 风险),必须指定具体域名;
  • 若允许携带 Cookie,Access-Control-Allow-Origin 不能为 *,且需配置 Access-Control-Allow-Credentials: true
  • 限制 Access-Control-Allow-MethodsAccess-Control-Allow-Headers 为必需值,避免过度开放。

其他跨域方案(特殊场景补充)

除了上面的核心方案,还有 2 种特殊场景的跨域方法,作为补充:

1. JSONP(仅支持 GET 请求,几乎淘汰)

原理:利用 <script> 标签不受同源策略限制的特性,通过动态创建 <script> 标签请求接口,后端返回回调函数包裹的 JSON 数据。

  • 缺点:仅支持 GET 方法、安全性差(可能注入恶意代码)、无法携带 Cookie;
  • 适用场景:极老的后端系统(不支持 CORS 和代理),现在几乎不用。
2. document.domain + iframe(仅适合同主域不同子域)

原理:若前端是 a.com,iframe 是 api.a.com(同主域不同子域),可通过 document.domain = 'a.com' 让两者同源,实现跨域通信。

  • 缺点:仅适合同主域、需操作 iframe,灵活性差;
  • 适用场景:老项目的 iframe 通信,现在很少用。

常见误区(必看,避免踩坑)

  1. 前端配置 CORS 头就能解决跨域:错!CORS 是后端返回的响应头,前端配置无效(浏览器只认后端返回的头);
  2. 生产环境用 Vite Proxy:错!Vite Proxy 仅用于开发环境,生产环境需用 Nginx 代理或 CORS;
  3. Access-Control-Allow-Origin: * 万能:错!有安全风险,且不支持携带 Cookie;
  4. 跨域请求不会发送:错!跨域请求会发送,浏览器会在「收到响应后」检查 CORS 头,若不允许则拦截响应(而非阻止请求发送);
  5. OPTIONS 请求是多余的:错!非简单请求(如 POST 带 JSON 数据、自定义头)会先发送 OPTIONS 预检请求,确认后端允许后再发送真实请求。

总结

跨域的核心是「浏览器同源策略限制」,解决方案的本质是「绕开或告知浏览器允许跨域」:

  • 开发环境:优先用 Vite Proxy(配置简单,无需后端配合);
  • 生产环境:优先用 Nginx 代理(前端无感知,安全性高)或 CORS 跨域头(后端单独搞定);
  • 避坑关键:分清环境、不滥用 *、理解 CORS 头的作用方(后端)。