Vite基础认识
1. webpack如何工作?
wp是每次冷启动都扫描依赖,全部打包成bundle.js,再启动开发服务器并加载bundle.js。每次修改重建bundle.js的片段。痛点是随着项目变大,HMR和冷启动都变慢。
2. vite如何工作
与wp不同。由于现代浏览器原生支持ESM:浏览器会按照import 发起 HTTP 请求拉模块。vite利用这点,在开发期间,不打包,先服务,按需编译。
index.html是vite的入口文件。启动dev服务器时,浏览器拿到import,vite对其进行拦截,只编译该文件返回JS。如果该文件又有import,继续递归编译。其他没有用到的不编译,切换路由再进行上述过程。HMR的时候,vite通过模块图找到了谁引用了这个模块,再结合HMR边界确定需要替换的模块并使用websocket通知vite client,重新请求该模块,精准替换,不必全局重建。
依赖预处理:依赖不完全是ESM模块,因此没办法直接编译,而且一个库可能引用非常多的模块。vite的做法是在首次启动项目的时候使用esbuild对被 import 用到的依赖预打包为单个ESM文件,缓存。
3. vite与webpack的区别
webpack与vite最大区别是架构不同。webpack是全局建立bundle,打包优先。而vite是服务优先,开发的时候使用ESM模块按需请求,只在build打包
4. 为什么 dev 用 esbuild、build 用 Rollup?
目标不同:dev 要"单文件转译尽量快"→esbuild(Go);build 要"产物最优"(tree-shaking/分包/压缩)→ Rollup。一次性 build的慢能接受,但每次按键都跑的 dev 必须快。
Esbuild预构建
1. Vite预构建是什么?解决什么问题?
dev 下源码走"原生 ESM 按需、不打包";依赖走"esbuild 预打包成单文件"
Vite预构建是将依赖统一打包成单文件放进缓存。解决了① 将CJS文件转换成ESM ② 将一个库的多个文件打包成一个单文件,减少请求次数 ③ 放入缓存提高重启速度。
DevTools Network 里,依赖请求长这样 /node_modules/.vite/deps/vue.js?v=xxxxx——单文件、带版本hash,而不是几百个零散请求
2. 为什么预构建用 esbuild 而不是 Rollup?
因为esbuild是Go写的,多核并行,速度比Rollup快很多。而且dev环境对速度要求很高。Rollup速度慢,留给build的时候用。
3. 预构建缓存在哪?什么时候失效?
预构建缓存在./node_modules/.vite/deps。当锁文件、依赖版本变化(Vite 启动时算一个 hash,和 _metadata.json 里旧的比对,不一致就重建。)、optimizeDeps 配置变化的时候失效,重新构建。可以使用vite --force强制重新构建。
4. 为什么 dev 不直接用 Rollup 打包所有东西?
Rollup增量速度慢,不适合每次改代码都跑。esbuild预构建+缓存的速度更快。
5. 遇到过"首次访问某页面 reload 一下"吗?为什么?
因为预构建是懒发现。当遇到没有预构建的import时,会重新触发预构建 + reload。在optimizeDeps.include 预热指定依赖可消除。
import.meta
import.meta是ESM规范提供的一个对象,用于暴露当前模块自己的元数据。
import.meta.url : 规范硬性要求(唯一保证有的)表示当前模块的完整 URL
import.meta.env : Vite 注入 用于表示环境变量
import.meta.glob : Vite 特有 用于表示运行时批量 import 一堆文件(前面动态选图用的)
import.meta.hot : Vite 特有 属于HMR API
静态资源的处理
1. public和src/assets的区别
public目录下的文件,在打包阶段会完全拷贝,不变化。而src/assets目录下的文件在使用的时候通过import引入,因此它也会放入构建流程。如果是小于4KB的图标,会被转为base64内联,减少请求次数。同时文件名会附上hash,内容变文件名才会变,有利于浏览器做长期缓存。public 的文件不加 hash、用绝对路径 /xxx 引用
2. 使用new URL设置路径引入
如果需要用URL的场景,没办法只用import,可以使用new URL('./example.js', import.meta.url)。需要注意的是这里的URL通常是API需要传入运行时的URL(比如说Web Worker),而不是源码的相对路径。因此vite会在构建的时候将new URL转换为最终静态资源的路径和文件名(带hash),与源码的文件名和路径完全不一样。
new URL第一个参数必须是字面量路径,不能拼变量(new URL(`./${name}.png`,...)不行)——要运行时动态选图,就用import.meta.glob。
3. 配合import.meta.glob 动态引入
import.meta.glob可以一次性引入多个静态资源,有默认懒加载和同步模式。在图片比较多的时候可以简化代码。
import.meta.glob返回一个map。
写:
const icons = import.meta.glob('./icons/*.png')
那么 icons 这个变量的值,构建后实际就是这样一个对象:(默认懒加载的情况下)
const icons = {
'./icons/sword.png': () => import('./icons/sword.png'),
'./icons/apple.png': () => import('./icons/apple.png'),
}
例子:
// 取值需要await
const photo = ref<string>('')
onMounted(async () => {
const mod = import.meta.glob<string>('../assets/test/*.jpg', {
import:'default',
})
const modules = mod['../assets/test/10.jpg']
if (modules)
photo.value = await modules()
})
如果加上了eager:true,则是同步加载的:
const icons = import.meta.glob('./icons/*.png', { eager: true })
// icons 长这样:
{
'./icons/sword.png': { default: '/assets/sword-hash.png' }, // 直接是对象
'./icons/apple.png': { default: '/assets/apple-hash.png' },
}
// 取值一步到位:
const url = icons['./icons/sword.png'].default // url = '/assets/sword-hash.png'
如果你设置了import:'default'那么import.meta.glob将直接返回default的值
Vite环境变量
1. vite内置变量
- import.meta.env.MODE:字符串("development"、"staging"、"production"),vite会根据MODE的值来加载对应模式下的环境变量,实现环境隔离。例如当
MODE = 'staging'时,Vite 会自动加载.env.staging中的配置。也可以使用JS代码判断当前环境,并作出对应的操作。 - import.meta.env.DEV 和 import.meta.env.PROD:布尔值。
DEV = true仅当MODE === 'development'PROD = true仅当MODE === 'production' - import.meta.env.BASE_URL:vite.config.ts 中的 base
- import.meta.env.SSR:是否运行在 SSR 环境
2. 自定义变量
自定义变量写在.env.*文件内。命名方式为.env.环境名,比如说.env.production会在npm run build 时加载。.env文件则会在所有的模式下都加载。.env.环境.local表示该模式下的本地私有配置。.env.local表示全局本地私有配置,所有模式都会加载。
自定义变量只有以VITE_作为前缀才能被读取。默认情况下自定义变量值的内容都视为字符串
覆盖优先级:启动命令已有的系统环境变量 > .env.[mode].local > .env.[mode] > .env.local > .env
3. 构建阶段的转换
在构建阶段,vite会根据MODE的选项,读取对应的环境变量,并将代码中的环境变量替换为Js字面量(比如字符串或者布尔常量),方便构建工具进行tree-shaking和打包。
MODE:这次使用哪套业务配置 DEV/PROD:这次属于开发运行还是生产构建,例如:
npm run dev -- --mode staging
那么import.meta.env.DEV为true,同时MODE为staging
4. preview
preview只是展示已经构建的dist,不会重新构建,因此也不会重新注入环境
5. 为自定义环境变量添加 TypeScript 类型
可以在.env.d.ts为自定义变量声明类型
/// <reference types="vite/client" />
// 1. 设置严格模式
interface ViteTypeOptions {
strictImportMetaEnv: unknown
}
// 2. 定义自定义变量类型
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string,
readonly VITE_APP_TITLE: string,
readonly VITE_SOURCE: string
}
// 3. 显式声明
interface ImportMeta {
readonly env: ImportMetaEnv
}
Vite 已经定义:
interface ImportMetaEnv {
BASE_URL: string
MODE: string
DEV: boolean
PROD: boolean
}
TypeScript 根据“同名且处于同一全局作用域”自动合并成完整的ImportMetaEnv。之后Ts就可以检查自定义变量的类型了。需要注意的是这里.env.d.ts是Ts加载的全局作用域类型,不需要export,否则就会变成模块,不能合并到全局的ImportMetaEnv类型了。
6. 建立环境配置边界
前面我们给环境变量定义了类型,现在我们封装集中配置模块,使得读取环境变量更加规范,避免变量不存在或者类型错误。
首先封装读取函数:
// 读取变量,拦截空值,空字符串和undefined
function required(name: string, value: string | undefined): string {
const normalizedValue = value?.trim()
if (!normalizedValue)
throw new Error(`[env] Missing required variable: ${name})
return normalizedValue
}
// 读取正整数
function positiveInteger(name: string, value: string | undefined): number {
const rawValue = required(name, value)
const normalizedValue = Number(rawValue)
if (!Number.isInteger(normalizedValue) || normalizedValue <= 0)
throw new Error(`[env] ${name} must be a positive integer`)
return normalizedValue
}
导出读取模块
export const appEnv = Object.freeze({
mode: import.meta.env.MODE,
apiBaseUrl: required('VITE_API_BASE_URL', import.meta.env.VITE_API_BASE_URL),
apiTimeout: positiveInteger('VITE_TIMEOUT', import.meta.env.VITE_API_TIMEOUT),
source: required('VITE_SOURCE', import.meta.env.VITE_SOURCE)
})
在需要读取变量的模块引入并使用即可
import { appEnv } from '@/config/env.ts' // 导入模块
console.table({
mode: appEnv.mode, // 读取
url: appEnv.apiBaseUrl,
source: appEnv.source,
timeout: appEnv.apiTimeout
})