Gerden Blog logo
|
|

Vite入门学习笔记

从零开始学习Vite

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内置变量

  1. import.meta.env.MODE:字符串("development"、"staging"、"production"),vite会根据MODE的值来加载对应模式下的环境变量,实现环境隔离。例如当 MODE = 'staging' 时,Vite 会自动加载 .env.staging 中的配置。也可以使用JS代码判断当前环境,并作出对应的操作。
  2. import.meta.env.DEV 和 import.meta.env.PROD:布尔值。DEV = true 仅当 MODE === 'development' PROD = true 仅当 MODE === 'production'
  3. import.meta.env.BASE_URL:vite.config.ts 中的 base
  4. 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
})

Comments

No comments yet.