在 Vue 项目里,jsconfig.json和tsconfig.json经常被当成“可有可无的编辑器配置文件”,直到某天@/components/Foo.vue在 IDE 里能跳转,打包时却报模块找不到;或者tsconfig.json里加了compilerOptions.paths,vue-tsc通过了,浏览器控制台还是 404。问题不在 Vue 本身,而在于这类配置同时被编辑器语言服务、类型检查器和构建工具三套系统读取,各读各的。compilerOptions是这些系统之间最重要的“共同语言”,配错一个字段,就可能出现“编辑器和终端各说各话”的局面。下面从文件分工、字段含义、Vue + Vite 落地配置、常见坑和排查链路几个角度,把jsconfig.json、tsconfig.json以及compilerOptions讲透,适合刚接触 Vue 配置、维护中大型项目、或者正从 JS 往 TS 迁移的开发者参考。
1. jsconfig.json 与 tsconfig.json:先搞清楚谁在影响你的 Vue 项目
1.1 两个文件不是二选一,而是面向不同语言服务
很多人第一次看到jsconfig.json,会以为它是 Webpack 或 Vite 的配置文件。实际上它和tsconfig.json属于同一家族:jsconfig.json是 TypeScript 语言服务为纯 JavaScript 项目准备的一份配置入口。也就是说,项目里没有 TypeScript 源码,但编辑器仍然需要知道“哪些文件属于这个项目”“@别名指向哪里”“是否对 JS 做类型检查”,这些信息就写在jsconfig.json里。
tsconfig.json则是 TypeScript 编译器tsc和编辑器 TypeScript 语言服务的标准配置。它不仅能控制类型检查,还能控制编译目标、模块格式、声明文件输出等。对于 Vue 项目来说,.vue单文件组件并不是标准 TypeScript 文件,真正接管它们的是 Volar 这类语言工具。但 Volar 并不会凭空理解路径别名、模块解析策略和全局类型,它依然要读tsconfig.json或jsconfig.json中的compilerOptions。
所以,两个文件不是“谁替代谁”的关系。纯 JS 项目通常用jsconfig.json,TS 项目通常用tsconfig.json。如果一个项目同时存在两个文件,就要特别小心:编辑器在不同目录、不同文件类型下可能选中不同配置,导致同一条import在一处正常、在另一处报错。
1.2 一条 import 要经过三套解析链路
先看一条最常见的 Vue 导入语句:
import UserCard from '@/components/UserCard.vue'这行代码至少会经过三套解析链路。
第一套是编辑器语言服务。VS Code 内置的 TypeScript 语言服务会读取最近的tsconfig.json或jsconfig.json,用baseUrl、paths、moduleResolution等字段判断@到底代表哪个目录,以及.vue文件能否被识别。如果配置正确,鼠标悬停能看到类型,Ctrl点击能跳转。
第二套是类型检查器。项目里运行vue-tsc --noEmit时,它同样依赖tsconfig.json。此时报错通常是“找不到模块”或“类型不匹配”。类型检查器关心的是声明和类型,不会真正打包代码。
第三套是构建工具。Vite、Webpack、Rollup 这些工具负责把源码变成浏览器能运行的产物。它们不一定读tsconfig.json中的paths。以 Vite 为例,Vite 默认不会自动读取tsconfig的路径别名,通常要在vite.config.ts里配置resolve.alias,或者使用额外插件把paths同步过去。
三套链路任何一套没对齐,就会出现“IDE 正常、构建失败”或“构建正常、IDE 满屏红线”。配compilerOptions的本质,就是让这三套系统尽量用同一套规则理解项目。
1.3 纯 JS、纯 TS、渐进迁移该怎么放文件
纯 JavaScript 的 Vue 项目,建议只放jsconfig.json。它可以提供别名跳转、全局类型提示、基础语法检查。如果团队暂时不想引入 TypeScript,就不要为了“看起来高级”硬塞一个tsconfig.json,否则两个配置可能互相干扰。
纯 TypeScript 的 Vue 项目,通常只放tsconfig.json。现代 Vite 模板还会拆成tsconfig.json、tsconfig.app.json、tsconfig.node.json,根配置只负责引用子项目。这样做的好处是浏览器端代码和 Node 端脚本分开检查,避免vite.config.ts被浏览器环境的lib影响。
如果项目正在从 JS 迁移到 TS,推荐的策略是保留一个tsconfig.json,开启allowJs: true,让 TS 语言服务同时处理 JS 和 TS 文件。对于还没准备好类型检查的 JS 文件,不要急着开checkJs,可以先用// @ts-check在单个文件上试验。等团队适应后,再逐步扩大范围。这样做比维护两个配置文件更稳,也更容易排查问题。
2. compilerOptions 字段拆解:别只抄模板,要看每个参数在管什么
2.1 baseUrl 和 paths:别名能不能跳转全靠它
baseUrl和paths是 Vue 项目里最常改的一对配置。baseUrl表示解析非相对模块时使用的基准目录,通常写成".",也就是当前tsconfig.json所在目录。paths则是在baseUrl基础上定义路径映射,例如:
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }这表示所有以@/开头的导入,都去src/目录下找。注意paths的键和值都是数组或字符串模式,"@/*"中的*会捕获剩余路径。例如@/components/UserCard.vue会被映射为src/components/UserCard.vue。
现代 TypeScript 允许省略baseUrl,paths会相对于tsconfig.json所在目录解析。但不少工具链和旧模板仍然依赖baseUrl,所以为了兼容性,建议保留"baseUrl": ".",或者至少确认项目里的 Vite、Webpack、Volar 版本都支持无baseUrl的写法。
这里最容易踩的坑是“只配了 TS,没配构建工具”。paths只影响语言服务和类型检查,不会自动改变 Vite 的解析结果。你必须在vite.config.ts中配置对应的resolve.alias。另外,别同时写多条含义冲突的别名,例如既有"@/*": ["src/*"],又有"@utils/*": ["src/utils/*"],虽然合法,但排查起来更麻烦。保持别名简洁,团队约定一个@指向src就够了。
2.2 target、module、moduleResolution:模块解析三件套
target决定 TypeScript 把代码当作哪个 ECMAScript 版本进行语法检查。Vue + Vite 项目中,实际转译和降级通常由 esbuild 或 Rollup 完成,tsc多数只做类型检查。因此target可以设得比较新,例如ES2020、ES2022或ESNext。设置得太旧,某些新语法会被误报;设置得太新,如果团队还要兼容旧环境,构建工具会负责降级,但类型层面可能不会提示兼容问题。
module决定生成的模块格式。Vue + Vite 项目通常使用ESNext,因为 Vite 本身就是基于原生 ES 模块的开发服务器。不要随手写成CommonJS,否则和 Vite 的模块体系格格不入。
moduleResolution是很多“找不到模块”问题的根源。它决定 TypeScript 如何查找模块声明文件。常见取值有:
| 取值 | 适用场景 | 特点 |
|---|---|---|
node | 旧版 Node 项目、旧工具链 | 兼容传统node_modules查找,但不理解 package.json 的exports字段 |
node16/nodenext | Node 服务端、Node 脚本 | 严格遵循 Node 的模块解析规则,对扩展名和exports更敏感 |
bundler | Vite、Rollup、esbuild 等打包器项目 | 支持exports,允许省略扩展名,适合现代前端项目 |
Vue + Vite 的浏览器代码通常使用"moduleResolution": "Bundler"。如果这里写成node,某些依赖只提供exports字段时,类型可能找不到,出现“编辑器能跑、类型检查报错”的现象。而vite.config.ts这类 Node 环境文件,则应放在单独的tsconfig.node.json中,使用node16或nodenext更合适。
2.3 strict 系列:Vue 模板类型检查比你想的更依赖它
strict: true是一组严格检查的总开关,包含noImplicitAny、strictNullChecks、strictFunctionTypes等。很多团队在项目初期为了快速开发,把strict设为false,后来想开启时发现满屏报错。这不是 Vue 的问题,而是之前大量隐式any和可能为null的值被放过了。
在 Vue 单文件组件中,strict影响非常直接。比如props没有写默认值,模板里直接访问,开启strictNullChecks后可能提示“可能为 undefined”。再比如ref没有初始值,类型推断可能不符合预期。Volar 会把这些错误标在模板和脚本上,vue-tsc也会在终端报出来。
开启strict的合理顺序是:先开noImplicitAny,解决明显缺少类型的问题;再开strictNullChecks,处理空值分支;最后开启完整的strict。如果一次性全开,老项目可能连构建都过不去。团队可以把strict当作长期目标,但不要在没有评估工作量的情况下直接合入主干。
2.4 noEmit、isolatedModules、skipLibCheck 的取舍
noEmit: true表示 TypeScript 只做类型检查,不输出编译产物。Vue + Vite 项目中通常必须开启,因为真正的产物由 Vite 生成。如果不开,vue-tsc或tsc可能生成一堆.js文件,污染源码目录。
isolatedModules: true要求每个文件都能被独立转译。Vite、esbuild 这类工具是单文件转译的,不会像传统tsc那样做跨文件类型分析。开启后,不能使用const enum,类型导入也建议写成import type。它能让类型检查和构建工具的行为更一致,减少“类型检查通过、构建失败”的概率。
skipLibCheck: true会跳过node_modules中声明文件的类型检查。大多数项目都会开启,因为它能显著提升检查速度,也能避开第三方库之间的类型冲突。但要注意,它可能掩盖依赖包内部的类型错误。如果项目升级依赖后出现诡异类型问题,可以临时设为false看完整报错,再决定是否回退依赖。
2.5 allowJs、checkJs、jsx 在混合项目中的边界
allowJs: true允许 TypeScript 语言服务处理.js文件。对于正在迁移的 Vue 项目很重要,它让 JS 和 TS 文件可以互相导入,而不会因为扩展名被拒绝。checkJs: true则进一步要求对 JS 文件做类型检查,通常伴随大量报错,不建议在老项目里直接全局开启。
更稳的做法是:在tsconfig.json中开启allowJs,但关闭checkJs,然后在个别 JS 文件顶部加// @ts-check,让这些文件先接受检查。等文件清理得差不多了,再逐步扩大范围。
jsx字段主要影响.tsx和 JSX 语法。如果项目使用 Vue 的 JSX 插件,需要设置合适的jsx值,例如preserve或react-jsx,并确保 Vite 插件正确配置。如果项目只用.vue模板,不用 JSX,这个字段影响不大,但不要乱写,否则可能让.tsx文件解析异常。
3. 直接能用的配置:Vue + Vite 下 jsconfig 与 tsconfig 落地
3.1 纯 JS 的 Vue 项目:jsconfig.json 推荐配置
如果项目是 Vue 3 + Vite + JavaScript,没有 TypeScript 源码,可以用下面这份jsconfig.json作为起点:
{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "Bundler", "baseUrl": ".", "paths": { "@/*": ["src/*"] }, "allowJs": true, "checkJs": false, "jsx": "preserve", "types": ["vite/client"], "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": [ "src/**/*.js", "src/**/*.vue", "src/**/*.jsx", "src/**/*.json", "src/**/*.d.ts" ], "exclude": ["node_modules", "dist"] }include中的src/**/*.vue很关键。如果漏掉,Volar 可能无法把.vue文件纳入项目,导致模板里的组件跳转失效。types: ["vite/client"]让import.meta.env这类 Vite 注入的全局类型生效。如果你在types里写了具体数组,只有数组中的类型包会被自动包含,所以不要随手把团队需要的全局类型排除掉。checkJs保持false,等需要时再局部开启。
3.2 TS + Vue 的 tsconfig 拆分:根、app、node
现代 Vite + Vue + TS 模板通常拆成三个文件。根tsconfig.json只负责引用:
{ "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } ] }tsconfig.app.json管浏览器端代码:
{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "preserve", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { "@/*": ["src/*"] }, "types": ["vite/client"] }, "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.d.ts"] }tsconfig.node.json管 Node 环境文件,例如vite.config.ts:
{ "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "Bundler", "allowSyntheticDefaultImports": true, "strict": true, "noEmit": true }, "include": ["vite.config.ts"] }拆分的原因很简单:浏览器代码需要DOM、DOM.Iterable这些库,而 Node 配置文件不需要;浏览器代码不应该访问process,Node 配置又需要 Node 类型。混在一起会让lib和types互相妥协,最后两边都不严谨。根配置用references组合子项目后,运行vue-tsc -b可以按项目引用方式检查,效率更高,也更接近大型项目的组织方式。
3.3 Volar、vue-tsc 与 vueCompilerOptions
Volar 负责在编辑器里解析.vue文件,vue-tsc负责在终端做类型检查。两者都依赖tsconfig.json,但.vue模板有一些额外规则,可以通过vueCompilerOptions配置。它通常写在tsconfig.json的顶层,和compilerOptions平级:
{ "compilerOptions": { "strict": true }, "vueCompilerOptions": { "target": 3.4, "strictTemplates": true } }strictTemplates: true会让模板中的类型检查更严格,例如组件 props 类型不匹配、事件参数错误都会报出来。对于新项目,建议开启;对于老项目,可以先保持默认,逐步修复模板类型问题。target用于告诉 Volar 当前 Vue 版本,避免模板编译行为不一致。如果团队发现模板类型提示异常,先确认 Volar 扩展是否启用、工作区 TypeScript 版本是否和项目一致,再检查vueCompilerOptions是否被放错层级。
3.4 Vite alias 与 tsconfig paths 必须对齐
tsconfig.json里的paths只解决“编辑器怎么找”,Vite 还需要自己的resolve.alias。常见写法如下:
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })这里用fileURLToPath是为了得到绝对路径,避免不同系统下相对路径解析差异。Windows 上路径大小写不敏感,Linux 和 CI 环境大小写敏感,所以forceConsistentCasingInFileNames也值得开启。不要一边在tsconfig里写@/*,一边在 Vite 里写@指向另一个目录,这种“同名不同义”非常难查。
如果不想手写两遍,可以使用vite-tsconfig-paths插件,让 Vite 直接读取tsconfig的paths。但要留意插件版本和 Vite 版本的兼容性,以及 monorepo 中多个tsconfig的读取顺序。核心原则始终是:编辑器、类型检查器、构建工具三处解析同一个别名时,必须得到同一个真实路径。
3.5 环境变量与全局类型:env.d.ts 不该被忽略
Vite 项目里经常用import.meta.env.VITE_API_BASE_URL。如果没有引入 Vite 客户端类型,TypeScript 会报Property 'env' does not exist on type 'ImportMeta'。通常需要在src/env.d.ts或根目录env.d.ts中写:
/// <reference types="vite/client" /> interface ImportMetaEnv { readonly VITE_API_BASE_URL: string readonly VITE_APP_TITLE: string } interface ImportMeta { readonly env: ImportMetaEnv }同时,tsconfig的include必须包含这个.d.ts文件。很多人改了env.d.ts但没生效,是因为文件不在include范围内,或者被exclude排除了。另外,types: ["vite/client"]和三斜线引用/// <reference types="vite/client" />选一种即可。团队要统一约定,避免一个项目里两种方式混用,最后不知道哪个在生效。
4. 配置失效与报错排查:从现象反推是哪一层没对上
4.1 别名在编辑器能跳,构建却报找不到模块
这是最经典的“三套链路不一致”。现象是 VS Code 里@/utils/request可以跳转,vue-tsc也不报错,但运行npm run build或npm run dev时,Vite 报Failed to resolve import "@/utils/request"。
原因通常只有一个:Vite 没有读到tsconfig的paths。解决方式是检查vite.config.ts中的resolve.alias是否存在,或者是否使用了同步paths的插件。还要检查 alias 的键是否带了@,是否和tsconfig中的@/*匹配。有些插件只处理paths中不带星号的映射,遇到通配符别名会失效,这时手动配resolve.alias更稳。
4.2 构建正常,编辑器满屏红线
反过来,构建能过、浏览器能跑,但编辑器一片红,通常是语言服务没把当前文件纳入项目。第一,检查include是否包含.vue、.ts、.tsx、.d.ts。第二,检查 VS Code 打开的工作区根目录是不是项目根目录,如果只打开了src子目录,可能读到错误的配置。第三,确认 Volar 已启用,并且没有同时安装旧版 Vetur 造成冲突。第四,执行“TypeScript: 重启 TS 服务器”或重启编辑器,让配置重新加载。
如果只有.vue文件报错,而.ts文件正常,重点看 Volar 和vueCompilerOptions。如果所有文件都报错,重点看根tsconfig是否被正确识别,以及工作区 TypeScript 版本是否和项目依赖一致。
4.3 Cannot find module './xxx.vue' 的几种来源
早期 Vue 3 项目经常在env.d.ts中写:
declare module '*.vue' { import type { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component }Vue 3 + Volar 下,这种声明通常不再需要,因为 Volar 能直接理解.vue文件。如果仍然报Cannot find module './xxx.vue',可能原因包括:moduleResolution设置不兼容、include没包含.vue、Volar 未启用、或者项目里存在旧的shims-vue.d.ts与新工具链冲突。排查时先删掉旧声明,确认 Volar 正常,再按需加回最小声明。
4.4 改了 tsconfig 不生效的几种隐蔽原因
第一,编辑器读取的不是你改的那个文件。VS Code 的 TS 服务会从当前文件向上查找最近的tsconfig.json,如果项目里有多个配置文件,可能命中的是子目录的配置。可以用命令面板里的“TypeScript: 选择 TypeScript 版本”旁边的“显示配置”功能,或者运行npx tsc --showConfig查看最终生效配置。
第二,根tsconfig.json用了files: []和references,这种配置本身不包含任何文件,直接改它不会影响子项目。要改的是tsconfig.app.json或tsconfig.node.json。第三,composite项目会生成.tsbuildinfo缓存,偶尔出现旧状态,可以删除后重试。第四,VS Code 使用的 TypeScript 版本可能是内置版本,而不是项目依赖版本,建议在命令面板中切换为“使用工作区版本”。
4.5 moduleResolution 选错导致的依赖类型解析异常
moduleResolution一旦选错,症状往往不是直接报“配置错误”,而是某个第三方库找不到类型。例如某些包只在package.json中提供exports字段,使用"moduleResolution": "node"时,TypeScript 看不到包内的类型声明;换成"Bundler"后立刻正常。反过来,在 Node 脚本中使用"Bundler",可能掩盖 Node 严格的模块解析规则,导致运行时才出现扩展名问题。
判断方法很简单:浏览器端、Vite 处理的前端代码,优先Bundler;Node 端、需要遵循 Node 模块规则的文件,优先node16或nodenext。不要为了省事让整个项目都用一种解析策略,拆分tsconfig.app.json和tsconfig.node.json才是长期稳定的做法。
5. 一套可复现的排查链路:拿到报错先别乱改
5.1 先确认当前文件归谁管
遇到配置问题,第一步不是改配置,而是确认“现在是谁在报错”。在 VS Code 中打开出错文件,查看它命中的tsconfig。可以在终端运行:
npx tsc --showConfig如果项目使用项目引用,可以加-p指定:
npx tsc --showConfig -p tsconfig.app.json输出中会包含最终生效的compilerOptions、include、exclude、files。把它和你在编辑器里看到的配置对照,能快速发现“改了但没生效”的问题。很多人花半天改根配置,最后发现实际生效的是子配置,这类时间成本完全可以通过--showConfig避免。
5.2 把构建和类型检查拆开跑
第二步,把构建和类型检查分开验证。运行:
npm run build再单独运行类型检查:
npx vue-tsc --noEmit如果vue-tsc通过、构建失败,问题更可能在构建工具的别名或插件配置;如果vue-tsc失败、构建通过,问题更可能在类型、模块解析或include范围。如果两者都失败,先解决类型检查报错的根源,再回头看构建,因为构建工具通常只给出模块级错误,不如类型检查器具体。
5.3 用最小配置法定位冲突
第三步,如果配置字段太多、不知道谁导致问题,用最小配置法。复制一份tsconfig,只留最基本的include、compilerOptions.paths、moduleResolution,然后逐项加回你怀疑的字段。每加一项就重启 TS 服务和类型检查。这样虽然看起来笨,但比在一大堆配置里猜要快得多。
尤其适合排查strict、checkJs、types、skipLibCheck这类互相影响的选项。比如types: ["vite/client"]写上去后,某些全局类型消失了,说明之前依赖了自动包含的@types包;checkJs开启后某个 JS 文件报错,说明这个文件需要先加// @ts-nocheck或补齐注释。
5.4 对齐三处配置清单
最后,把三处配置列成清单逐项核对:
| 配置位置 | 需要确认的项 | 常见问题 |
|---|---|---|
tsconfig.json/jsconfig.json | baseUrl、paths、include、moduleResolution | 别名缺失、.vue未包含、解析策略不匹配 |
| Vite / Webpack 配置 | resolve.alias、插件顺序、扩展名解析 | TS 能跳转但构建报模块找不到 |
| 编辑器语言服务 | Volar 是否启用、TS 版本、工作区根目录 | 缓存旧配置、Vetur 冲突、打开的目录不对 |
这三处任何一处缺失,都会出现“看起来配置了,实际没生效”的错觉。团队里最好把这份清单写进项目文档或 README 的“常见问题”部分,新人接手时能少走很多弯路。
5.5 把配置固定成可检查的工程习惯
配置本身不是一次性工作。依赖升级、Vite 版本变化、TypeScript 版本变化,都可能让原本正常的compilerOptions出现新问题。建议在package.json中固定类型检查脚本:
{ "scripts": { "typecheck": "vue-tsc --noEmit -p tsconfig.app.json", "build": "npm run typecheck && vite build" } }这样每次构建前都会先跑类型检查,避免把类型错误带到产物阶段。如果项目拆分了项目引用,也可以使用vue-tsc -b。另一个习惯是锁定关键依赖版本,不要频繁跨大版本升级 Vite、Vue、TypeScript 和 Volar,尤其在生产项目里,配置和依赖版本需要一起评估。
我自己的经验是,jsconfig.json和tsconfig.json本身不玄学,真正麻烦的是它们同时被编辑器、类型检查器和构建工具读取,而每套系统对compilerOptions的敏感程度不同。遇到报错时,先确认文件归谁管,再把构建和类型检查拆开跑,最后用最小配置法定位冲突。路径别名尤其要记住:tsconfig里的paths和 Vite 里的resolve.alias必须指向同一个真实目录,否则 IDE 和终端迟早会分道扬镳。另一个小技巧是,在.vscode/settings.json中固定工作区 TypeScript 版本,并让团队统一使用 Volar,能省掉大量“我这里正常、你那里报错”的沟通成本。