当我们使用 Vite 搭建起一个现代化的前端项目时,Vite 提供了一些非常强大的特有功能(如环境变量、静态资源处理)。但由于这些功能是 Vite “独有” 的,原生的 TypeScript 并不认识它们。
这就是为什么我们经常会看到代码能跑,但编辑器里却满屏飘红报错的原因。今天,我们就来给 Vite 的这些特有功能加上 TypeScript 约束。
扩展 ImportMetaEnv 接口
在 Vite 中,我们可以使用 import.meta.env.VITE_XXX 来访问环境变量。但在默认情况下,TypeScript 只知道几个内置的变量(如 MODE、DEV),如果你自定义了一个 VITE_API_URL,那么TypeScript 会立刻报错,并提示该属性不存在。
1. 为什么要扩展接口?
这本质上是一个 “模块扩展(Module Augmentation)” 的问题。我们需要在不改动 Vite 源码的前提下,利用接口合并的特性,把我们自己的变量名塞进 Vite 的类型定义里。
示例 1:为自定义环境变量注入类型提示
// src/env.d.ts 或 src/vite-env.d.ts
// 1. 扩展 Vite 预留的环境变量接口
interface ImportMetaEnv {
// 注入我们业务中自定义的变量
readonly VITE_APP_TITLE: string;
readonly VITE_API_URL: string;
readonly VITE_BUILD_VERSION: string;
}
// 2. 将自定义变量合并注入 ImportMeta
interface ImportMeta {
readonly env: ImportMetaEnv;
}分析:
编写完这段声明之后,再回到业务逻辑中,当我们敲下 import.meta.env. 的那一刻,编辑器会自动弹出 VITE_API_URL 等选项。这种做法可以杜绝因为手误拼错环境变量名而导致的生产环境连接失败,大大提升了系统的健壮性。
编写 env.d.ts 声明文件
在 TypeScript 项目中引入 .vue、.scss 或者 .svg 图片时,小伙伴们一定见过这个报错:
无法找到模块 xxx 或其相应的类型声明这是因为在默认情况下,TypeScript 只认识 “.ts” 和 “.js” 文件。对于其他非代码资源,我们需要在 env.d.ts(也叫 vite-env.d.ts)中手动编写 “通配符声明” 才行。
示例 2:非代码资源的全局声明
// src/env.d.ts
// 注入 Vite 客户端基础类型
/// <reference types="vite/client" />
// 告诉 TS:所有的 .vue 文件都是一个组件类型
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
// 注意:引入了 vite/client 后,像 *.svg、*.scss 等常见静态资源的声明其实官方已经帮我们做好了,
// 这里手动写出来是为了让你理解底层的 declare module 原理。
declare module "*.svg" {
const content: string;
export default content;
}分析:
/// <reference types="vite/client" />文件最顶部的这一句代码,其实是 TypeScript 的三斜线指令。我们只要加上这一行,Vite 就会把内置的、关于常用静态资源(如图片、CSS 模块)以及内置环境变量(如 import.meta.env.MODE)的声明全部自动注入进来。
Vite 的 “双 TSConfig” 机制
如果小伙伴们使用的是较新的 Vite 脚手架,此时会发现项目中存在多个 tsconfig.ts 文件。很多初学者为了省事,会把它们全删了合并成一个,这其实破坏了 Vite 的隔离设计。
1. 为什么要拆分配置?
在 Vite 项目中,我们的代码实际上运行在两个完全不同的环境中:
- 浏览器环境:我们写的 .vue、.tsx 和业务 .ts 代码,最终是跑在浏览器里的(不需要 Node.js 的内置模块如 fs 或 path)。
- Node 环境:而 vite.config.ts 这个配置文件,是在打包阶段跑在 Node.js 环境里的(它需要 fs、path,但不认识 DOM 里的 window 或 document)。
如果只用一个 tsconfig.json,那么 TypeScript 就会把这两种环境的类型混为一谈,导致你在业务代码里错误地调用 Node API 而编辑器不报错。
2. 官方推荐的配置方案
Vite 官方推荐将不同的配置拆分到不同的文件中:
tsconfig.app.json:专门用来约束 src 目录下的业务代码(包含 DOM 类型,没有 Node 类型)。tsconfig.node.json:专门用来约束 vite.config.ts 等工程化脚本(包含 Node 类型,没有 DOM 类型)。tsconfig.json:作为一个总入口,利用 references 字段将上面两个文件组合起来。
这种隔离机制是现代工程化的标配。当我们要在业务代码中配置 “@” 路径别名时,必须把它写在 tsconfig.app.json 的 compilerOptions.paths 中,而不是写在给 Node 用的配置里。
Vite 路径别名配置
我们在 tsconfig.json 中配置了 paths 来实现 “@/” 别名导入。但正如之前强调的,tsconfig.json 只是为了让编辑器不报错,想要让 Vite 真正能通过这个路径找到文件,我们必须在 vite.config.ts 中进行镜像配置。
示例 3:利用 defineConfig 与 ESM 规范配置别名
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// 引入 Node.js 原生的 URL 解析模块(适配 ESM)
import { fileURLToPath, URL } from "node:url";
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
// 将 "@" 映射到 src 目录的最佳实践
"@": fileURLToPath(new URL("./src", import.meta.url))
}
}
});分析:
由于现代前端项目普遍采用 ES Modules 规范,传统的 __dirname 全局变量已经失效。我们通过 new URL("./src", import.meta.url) 先获取当前配置文件的绝对 URL,然后再使用 Node.js 的 fileURLToPath 将其转换为安全的文件系统路径。
只有配置了这里,Vite 的打包引擎才知道你代码里的 “@” 到底指向哪里。
Vite 类型检查陷阱:配合 vue-tsc 守住底线
在使用 Vite 开发时,有一个无数初学者都会踩的巨坑:Vite 底层使用的是 esbuild 进行编译,而 esbuild 为了追求极致的速度,它在打包时会直接剥离 TS 类型,根本不做任何类型检查!
也就是说,即使你在代码里写了极其离谱的类型错误(比如把字符串赋值给数字),只要编辑器没开或者你没注意看波浪线,执行 npm run build 时依然会打包成功,并把带着 Bug 的代码推向生产环境。
为了守住类型安全的底线,我们必须在打包命令中强行加入类型检查。
示例 4:修改 package.json 的构建脚本
{
"scripts": {
"dev": "vite",
// 错误做法:直接 build,忽略所有 TS 类型报错
// "build": "vite build",
// 严格模式:使用 -b (build mode) 遍历检查所有子配置,零报错才打包
"build": "vue-tsc -b && vite build"
}
}分析:
vue-tsc 是 Vue 官方提供的基于 TypeScript 的类型检查工具(它甚至能检查 .vue 文件里的 HTML 模板类型)。
需要特别注意的是,这里我们使用了 -b(即 --build 模式)而不是旧版的 --noEmit。因为正如我们在前文所说,现代 Vite 项目采用了 “双 TSConfig” 的隔离机制,根目录的 tsconfig.json 只负责组合,本身不包含文件。如果使用 --noEmit,它什么都不会检查直接放行;而 -b 模式会强制顺着 references 找到 tsconfig.app.json 和 tsconfig.node.json 并对它们进行严格的类型审查。
通过 && 符号连起来,我们的构建流程就变成了:先让 vue-tsc 严格审查全项目的类型,如果发现哪怕一个报错,构建就会立刻中止;只有当类型 100% 正确时,才会安全地交给 Vite 进行最后的打包编译。
