vite

67 阅读13分钟

介绍

总览

  • 开发服务器,基于原生ES模块提供内建功能 模块热替换
  • 一套构建指令 rollup 打包代码,预配置 输出优化过的静态资源
  • 通过插件 vite 支持与其他框架和工具集成

须知

index 和根目录

  • 开发期间vite 相当于服务器,index.html 是入口文件
  • vite 解析 script type="module 标签指向JS源码
  • CSS 的link href 也利用vite 解析
  • 支持多页面应用模式

选择vite

为啥比传统打包工具更快

传统打包工具的问题

在原生支持ES模块之前,开发者需要用打包工具 webpack rollup 将多个模块合并成单个文件

import { add } from './math.js';
console.log(add(1, 2));
  • 问题一 启动慢 所有代码打包后才能启动开发服务器
  • 问题二 更新慢 修改文件后 需要重新打包整个应用或大部分代码

vite 解决方法

1.快速启动-依赖预构建

vite将代码分成两类 依赖 不变动的第三方库 和 源码

// Vite 会预构建的依赖
import React from 'react'; 
import lodash from 'lodash';

// 开发者源码 - 按需转换
import MyComponent from './MyComponent.vue';
2.按需编译 -原生ESM

vite 利用浏览器支持ES模块特性

<!-- 浏览器直接请求 ES 模块 -->
<script type="module" src="/src/main.js"></script>

当浏览器请求的时候,vite才转为对应文件

  1. 请求app.vue ---> vite及时编译JS
  2. 请求style.css ---> vite将其转化为JS模块注入样式
3.快速热更新
  • 传统打包工具HMR需要重建整个模块图修改 ComponentA → 重建整个打包文件 → 浏览器全量更新
  • vite HMR 只更新变化的模块 修改 ComponentA → 仅编译 ComponentA → 浏览器只更新 ComponentA

生产环境也要打包

  • 虽然开发用原生ESM也好,但是生产环境仍要打包
  • 减少请求次数
// 未打包时会产生多个请求
import a from './a.js'; // 请求1
import b from './b.js'; // 请求2
  • 性能优化 tree-shaking 代码分割

为什么不用 ESBuild 打包?

虽然 Vite 用 esbuild 预构建依赖,但生产打包仍用 Rollup,因为

  1. Rollup 插件生态更丰富
  2. Rollup 对代码分割等特性支持更好
  3. 未来会转向 Rolldown(Rust 版 Rollup)

实际实例对比

传统打包工具流程

  1. npm run dev 启动
  2. 打包所有文件(30秒)
  3. 启动服务器
  4. 修改文件 → 重新打包(5秒)

Vite 流程

  1. npm run dev 启动(1秒)

  2. 浏览器按需请求文件

    • 访问 / → 请求 main.js
    • main.js 导入 App.vue → 请求 App.vue(即时编译)
  3. 修改 App.vue → 仅重新编译该文件(0.1秒)

功能

npm依赖解析和预构建

浏览器对 ES 模块的支持情况

  • 支持

    • 使用`
    • 支持import/export语法
    • 支持相对路径 如'./module.js'和绝对路径'/path/to/module.js'
  • 不支持

    • 裸露模块导入 如import 'lodash'
    • 直接解析node_modules中的包
  • 原因

    • 安全 浏览器不知道'loadsh' 具体对应那个URL
    • 无node.js算法 即浏览器没有 Node.js 的模块解析机制(如 node_modules 查找 )不知道如何从 'react' 解析到 /node_modules/react/index.js
    • *无包管理概念 - 浏览器本身没package.json 概念 无法确定模块版本和依赖关系

vite如何解决的

  • 预构建阶段
    • 扫描所有import "*" 将node_modules中的包转为浏览器中可以用的ESM格式

    • 将 CommonJS/UMD 模块转换为 ESM 格式

    • 合并多个小文件以减少请求数量

      node_modules/
         lodash/
           add.js
           subtract.js
           multiply.js
      
       会被预构建为:
       .vite/deps/
             lodash.js  # 合并后的ESM版本
      
    • 路径重新写

      // 开发前
           import { ref } from 'vue'
      
       // 开发时被Vite转换为
       import { ref } from '/node_modules/.vite/deps/vue.js?v=3a2b1c'
      
    • 开发服务器处理

      • 当浏览器请求/node_modules/.vite/deps/vue.js 返回预构建好的版本
    • 强缓存策略

      • Vite 对预构建的依赖使用强缓存(通过 Cache-Control: max-age=31536000,immutable
      • 同一依赖只会下载一次 除非手动修改 package.json 或 node_modules,否则不会重新请求

模块热替换

什么是HMR

hot module replacement 热模块替换 在不刷新整个页面情况下,实时添加删除替换模块

传统开发 vs Vite HMR 开发体验对比

传统开发流程(无HMR)
  1. 修改一个组件(如按钮颜色)
  2. 等待打包工具重新编译(3-5秒)
  3. 浏览器自动刷新页面
  4. 需要重新操作到之前的状态(如打开某个弹窗)
  5. 总耗时:10秒+
Vite HMR 开发流程
  1. 修改一个组件(如按钮颜色)
  2. Vite 立即推送更新(0.1秒)
  3. 只有该组件更新,页面不刷新
  4. 保持所有当前状态(如表单输入值、弹窗状态)
  5. 总耗时:0.2秒

Vite HMR 工作原理

1.客户端-服务端通信
浏览器 <--WebSocket--> Vite 服务器

当文件修改时,Vite 服务器通过 WebSocket 通知浏览器哪些模块需要更新

2. 更新过程
  1. 修改文件并保存
  2. Vite 检测到变化
  3. 只重新编译改动的模块
  4. 通过 WebSocket 发送更新信息
  5. 浏览器接收更新并替换旧模块
  6. 框架运行时(如 Vue/React)处理组件更新

ts

仅执行转译

graph TD
    A[你的TS代码] --> B{Vite处理}
    B -->|esbuild转译| C[浏览器可运行的JS]
    B -->|不处理| D[类型检查]
    D -->|需额外工具| E[TS类型错误提示]
  • 类型导入压缩优化
build: {
  minify: 'terser',
  terserOptions: {
    compress: {
      pure_getters: true,
      unused: true
    }
  }
}
  • vite 只做一件事:快速把TS 代码变成JS 不检查类型

  • 开发怎么类型检查

    • vscode自动提示
    • 加检查插件 npm install vite-plugin-checker -D
    //vite.config.ts
    import checker from 'vite-plugin-checker';
    
    export default {
    plugins: [
    checker({ typescript: true }) // 浏览器+终端都会报类型错误
    ]
    }
    
    • 总结
      • 开发用 vite-plugin-checker实时检查类型
      • 打包前 先运行 tsc --noEmit
      • 写代码 类型引入用import type

TS编辑器选项

1. isolateModulue:true
  • 确保每个文件独立编译
    // ❌ 错误写法(需要关闭isolatedModules才能用)
    
const enum Colors { // const enum 需要跨文件分析
  Red = 'RED'
}

   // ✅ 正确写法(开启isolatedModules时用)
enum Colors { // 改成普通enum
  Red = 'RED'
}

  • const enum TS 的特殊的枚举类型,编译阶段被删除,替换成值。
2.useDefineForClassFields 类字段定义方式
  1. 在class里直接写的属性
class Cat {
   name: string = "米米"
}
  1. 两种方式定义区别
// 假设有父类
class Animal {
  sound = '叫'
}

// 情况1:useDefineForClassFields = false
class Cat extends Animal {
  sound = '喵'  // 直接覆盖
}
// 运行 new Cat().sound => '喵'

// 情况2:useDefineForClassFields = true
class Cat extends Animal {
  constructor() {
    super()
    Object.defineProperty(this, 'sound', { value: '喵' })
  }
}
// 可能产生意外行为(取决于父类定义方式)
  1. 为啥要这个配置
  • 旧版库 如vue2 设置false
  • 现代项目 true
3.target

vite会忽略tsconfig.json 里的target用vite.config.js

  • 开发默认esnext 最新配置
  • 构建 用build.target
//vite config 
export default {
    esbuild:{
      target:"es2020"//开发生效
    }
    build:{
      target:"es2015"//构建生效
    }
}
🤔忽略为啥还要配置
  1. 双重作用 编译VS类型检查
工具用途依赖target配置
vite/esbuild代码转译 JS输出用自己配置 忽略tsconfig
ts类型检查 语法验证依赖tsconfig
 // tsconfig.json
    {
      "compilerOptions": {
        "target": "ESNext", // 这里只管类型检查的宽松度
        // 其他配置...
      }
    }
4.skipLibCheck:true
  • 跳过对node_modules里类型文件检查
5. extends
6.jsx
{
  "compilerOptions": {
    "target": "ESNext", // 虽然Vite不用,但给TS检查用
    "module": "ESNext",
    "strict": true,
    "isolatedModules": true, // 必须!
    "skipLibCheck": true,    // 推荐
    "esModuleInterop": true,
    "moduleResolution": "node",
    "useDefineForClassFields": true // 现代项目保持true
  },
  "include": ["src/**/*"]
}

客户端类型

Vite + TypeScript 类型系统完全指南

一、客户端类型配置

1. 基础配置

// src/vite-env.d.ts
// src/vite-env.d.ts
declare module '*.svg' {
  const content: React.FC<React.SVGProps<SVGElement>>
  export default content
}

declare module '*.css' {
  const classes: { readonly [key: string]: string }
  export default classes
}

interface ImportMetaEnv {
  readonly VITE_API_URL: string
  readonly VITE_APP_VERSION: string
}

/// <reference types="vite/client" /> 放在最后面

2. 特殊文件类型处理

  1. svgz作为react组件
  2. 图片资源类型

3.总结

  • 统一管理vite相关的类型声明
graph TD
    A[项目类型系统] --> B[常规.d.ts文件]
    A --> C[vite-env.d.ts]
    B --> D[全局类型扩展]
    B --> E[第三方库类型补丁]
    C --> F[Vite环境变量类型]
    C --> G[Vite资源模块声明]

HTML

html文件在vite中的角色

  • 入口文件 向浏览器指明要加载哪些文件
  • 支持单页面和多页应用

资源引用例子

<!-- 这些资源会被 Vite 自动处理和打包 -->
<script type="module" src="/src/main.js"></script>
<link rel="stylesheet" href="/src/styles.css" />
<img src="/src/images/logo.svg" alt="logo" />
  • vite支持script link img audio video
  • 包括meta标签
  • 忽略处理
    • 比如:引用CDN不让vite处理 可以添加vite-ignore
<!-- Vite 不会处理这个资源 -->
<script src="https://cdn.example.com/library.js" vite-ignore></script>
<img src="https://cdn.example.com/image.png" vite-ignore alt="外部图片">

JSX

  • vite对.jsx 和 .tsx 开箱即用

不同框架的JSX支持

  1. react 默认支持JSX
  2. vue 需要使用官方插件
//npm install @vitejs/plugin-vue-jsx
import vueJsx from '@vitejs/plugin-vue-jsx'

export default defineConfig({
  plugins: [vueJsx()]
})

  1. Preact
    • Preact 是轻量级js库,与react类似 体积更小。
    • 需要配置
export default defineConfig({
  esbuild: {
    jsxFactory: 'h',       // Preact 的创建元素函数
    jsxFragment: 'Fragment' // Fragment 组件
  }
})
  1. 为啥要这些配置
  • jsxFactory 告诉编辑器将JSX转为js函数调用
    • react默认使用 react.createElement
    • Preact使用h
    • vue有自己转化方式
  • jsxFragment 指定如何处理JSX中的空标签

CSS

基础CSS 支持

  • 在vite 中 直接导入.css文件,会将内容自动注入到style中 支持热更新
  • @import 支持
  • PostCSS 支持
    • 如果项目中有postcss.config.js文件 vite会用其中配置
    // postcss.config.js
    module.exports = {
      plugins: [
        require('autoprefixer'),  // 自动添加浏览器前缀
        require('postcss-nested') // 支持嵌套语法
      ]
    }
    

CSS 预处理器支持

- vite内置Sass/less支持
- stylus支持

CSS modules

  • 让CSS 类名局部化的技术,避免全局命名冲突,vite自动支持以.module.css结尾文件作为CSS modules
  • 使用
/* style.module.css */
.primaryBtn {
  background-color: blue;
  color: white;
}

.text-red {
  color: red;
}
import styles from './style.module.css'

function Button() {
  return (
    <button className={styles.primaryBtn}>
      点击我
    </button>
  )
}
  • 类名转化配置
export default defineConfig({
  css: {
    modules: {
      localsConvention: 'camelCase' // 将类名转换为驼峰式
    }
  }
})

使用

import { textRed } from './style.module.css'
// 相当于原来的 .text-red 类

禁止CSS 注入页面

基本用法

默认情况下,导入CSS会自动注入到页面中,使用?inline参数可以禁止

// 常规导入 - CSS 会自动注入到页面
import './style.css'

// 使用 inline 参数 - CSS 不会注入页面,而是作为字符串返回
import cssString from './style.css?inline'

console.log(cssString) // 输出 CSS 字符串内容
应用场景
  • 动态加载样式
// 根据条件动态添加样式
const darkMode = true
const styles = await import(darkMode ? './dark.css?inline' : './light.css?inline')

document.adoptedStyleSheets = [new CSSStyleSheet().replaceSync(styles.default)]
  • CSS-in-JS 库集成
import cssString from './styles.css?inline'

// 将 CSS 字符串用于 CSS-in-JS 库
const styles = someCSSinJSLibrary(cssString)

lightning CSS

Lightning CSS 是Rust编写的高性能的CSS处理工具,比传统的postCSS更快

// npm install -D lightningcss
//vite.config.js
export default defineConfig({
  css: {
    transformer: 'lightningcss', // 使用 Lightning CSS 替代 PostCSS
    lightningcss: {
      // Lightning CSS 配置选项
      cssModules: {
        // CSS Modules 配置
        pattern: '[hash]_[local]' //自定义类名生产模式
      }
    }
  },
  build: {
    cssMinify: 'lightningcss' // 使用 Lightning CSS 进行压缩
  }
})

静态资源处理

1. 基本资源导入

图片等不同资源 导入的静态资源都会返回最终解析后的URL

import imgUrl from './assets/logo.png'

// 在模板中使用
const img = document.createElement('img')
img.src = imgUrl
document.body.appendChild(img)

2.特殊查询参数

  1. ?url- 显式获取资源URL
    // 获取资源的最终URL(根据文件大小自动决定是否内联)
      import imgUrl from './image.png?url'
    
     console.log(imgUrl) 
     // 输出类似 /assets/image.abc123.png
    
  2. ?raw- 以原始字符串导入
    // 导入文件原始内容
    import shaderCode from './shader.glsl?raw'
    import svgContent from './icon.svg?raw'
    
    console.log(shaderCode) // 输出文件原始文本内容
    
  3. ?worker- 导入web Worker
    // 导入为Web Worker
    import Worker from './worker.js?worker'
    
    const worker = new Worker()
    worker.postMessage({ /* 数据 */ })
    
  4. ?worker&inline- 内置worker
    // Worker会被内联为base64字符串
    import InlineWorker from './worker.js?worker&inline'
    
    const worker = new InlineWorker()
    

JSON

1.默认导入

// 导入整个JSON对象
import config from './config.json'

console.log(config.apiUrl)

2.具名导入 支持tree shaking

// 只导入需要的部分,未使用的字段会被tree shaking掉
import { apiUrl, timeout } from './config.json'

console.log(apiUrl)

Glob 导入

基本用法

  1. 基本glob导入
// 导入目录下所有 JS 文件
const modules = import.meta.glob('./components/*.js')

// 使用示例
for (const path in modules) {
  modules[path]().then(module => {
    console.log(`加载 ${path} 成功`, module)
  })
}

相当于

const modules = {
  './components/Button.js': () => import('./components/Button.js'),
  './components/Modal.js': () => import('./components/Modal.js')
  // ...
}
  1. 立即导入 不是懒加载
// 立即导入所有匹配的模块
const modules = import.meta.glob('./utils/*.js', { eager: true })

// 可以直接使用
console.log(modules['./utils/helper.js'].formatDate(new Date()))

高级用法

1. 多模式匹配
// 匹配多个目录的文件
const modules = import.meta.glob([
  './components/*.js',
  './layouts/*.js',
  '!./components/Deprecated.js' // 排除特定文件
])
2.具名导入 tree shaking
// 只导入每个模块中的特定导出
const setups = import.meta.glob('./plugins/*.js', {
  import: 'setup' // 只导入 setup 函数
})

// 立即导入 + Tree Shaking
const configs = import.meta.glob('./configs/*.js', {
  import: 'default', // 导入默认导出
  eager: true
})
3.自定义查询参数
// 导入所有 SVG 文件作为原始字符串
const svgFiles = import.meta.glob('./icons/*.svg', {
  query: '?raw',
  import: 'default'
})

// 导入所有图片作为 URL
const imageUrls = import.meta.glob('./assets/*.{png,jpg}', {
  query: '?url',
  import: 'default'
})

webAssembly

访问webAssembly模块

node.js获取模块

web workers

  • web workers 允许在后台线程这运行脚本,避免阻塞主线程

通过构造器导入

基本用法

// 主线程代码
const worker = new Worker(new URL('./worker.js', import.meta.url), {
  type: 'module' // 指定为模块类型worker
})

worker.postMessage({ command: 'start', data: 42 })

worker.onmessage = (e) => {
  console.log('收到worker消息:', e.data)
}

worker文件

// worker.js - 模块化worker
self.onmessage = (e) => {
  if (e.data.command === 'start') {
    // 执行计算密集型任务
    const result = heavyCalculation(e.data.data)
    self.postMessage({ result })
  }
}

function heavyCalculation(input) {
  // 模拟耗时计算
  return input * 2
}

带有查询后缀的导入

基本worker导入

import MyWorker from './worker.js?worker'

const worker = new MyWorker()
worker.postMessage('hello')

内联worker 小文件适用

import InlineWorker from './worker.js?worker&inline'

// 这个worker会被内联为base64字符串,不会有额外网络请求
const worker = new InlineWorker()

获取worker URL

import workerUrl from './worker.js?worker&url'

// 可以手动控制worker加载
const worker = new Worker(workerUrl, { type: 'module' })

内容安全策略 CSP

内容安全策略CSP 一种安全机制,防止跨站脚本攻击。通过限制浏览器可以加载哪些资源增强应用安全性

nonce-{RANDOM}

// vite.config.js
export default defineConfig({
  server: {
    cspNonce: 'nonce-{RANDOM}' // 每次请求生成唯一的随机值
  }
})
  • 为所有的script和style 标签添加nonce属性
  • 注入标签
  • 开发生产都适用

效果

<head>
  <meta property="csp-nonce" nonce="dGhpcyBpcyBhIG5vbmNl">
  
  <!-- 内联脚本 -->
  <script nonce="dGhpcyBpcyBhIG5vbmNl">
    // Vite 客户端代码
  </script>
  
  <!-- 内联样式 -->
  <style nonce="dGhpcyBpcyBhIG5vbmNl">
    /* 内联 CSS */
  </style>
</head>

data

// vite.config.js
export default defineConfig({
  build: {
    assetsInlineLimit: 4096 // 默认值:小于4KB的资源内联为data URI
  }
})

CSP策略实例

<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  img-src 'self' data:; 
  font-src 'self' data:;
  style-src 'self' 'nonce-dGhpcyBpcyBhIG5vbmNl';
  script-src 'self' 'nonce-dGhpcyBpcyBhIG5vbmNl';
">
  • 禁止为 script-src 添加 data: 允许规则

    示例错误配置:script-src 'self' data:;

    这会使应用容易受到脚本注入攻击

  • 对于图片和字体可以使用 data:

    示例安全配置:img-src 'self' data:;

构建优化

CSS代码分割 默认启用

// vite.config.js
export default defineConfig({
  build: {
    cssCodeSplit: true // 默认值,可设置为false禁用
  }
})

原理

  • 每个异步 chunk 的 CSS 会被提取到单独文件

  • 当异步组件加载时,自动注入对应的 CSS

  • 避免 FOUC(无样式内容闪烁)

示例:

text
构建输出:
  assets/
    Home.abc123.js
    Home.def456.css   <-- 异步组件的CSS
    About.ghi789.js
    About.jkl012.css  <-- 另一个异步组件的CSS

禁止CSS分割

build: {
  cssCodeSplit: false // 所有CSS合并到单个文件
}

预加载指令生成

Vite 自动为入口 chunk 生成预加载指令:

<!-- 构建后的 index.html -->
<head>
  <link rel="modulepreload" href="/assets/main.abc123.js">
  <link rel="modulepreload" href="/assets/vendor.def456.js">
</head>

优势:

  • 浏览器提前加载关键资源

  • 减少关键渲染路径的往返次数

  • 提升页面加载性能

异步chunk加载优化 智能依赖分析

未优化场景

Entry -> 请求A -> A解析 -> 发现需要C -> 请求C
       (网络请求)  (解析时间)   (网络请求)

vite优化后

Entry -> 同时请求A和C
       (并行请求)

优化原理

graph LR
  A[入口文件] --> B[异步chunk A]
  A --> C[异步chunk B]
  B --> D[公共chunk C]
  C --> D[公共chunk C]
  
  
  • 优化前: A加载后才请求C
  • 优化后: A和C同时加载