Vite+Vue3+TypeScript完整项目实战教程:从搭建到工程化配置
简介这是一份面向前端开发者与Vue进阶学习者的实战型项目模板聚焦Vite构建工具、Vue 3组合式API及TypeScript工程化实践帮助开发者快速搭建现代化、可维护的单页应用基础架构。资源包共107个文件涵盖23个Vue组件文件含响应式逻辑与自定义Hook、10个TypeScript类型定义与业务逻辑文件、24个SCSS与14个LESS样式资源支持主题定制与模块化管理、13个PNG/SVG/ICO等静态资源以及HTML入口、环境配置、字体图标woff2/eot/ttf等和构建配置文件整体仅1.32MB轻量且开箱即用。已有3047人学习下载说明其在真实开发场景中具备较高参考价值。读者可直接运行调试深入理解Vite热更新机制、TS类型约束在Vue组件中的落地方式、CSS预处理器集成策略以及生产/开发环境变量分离等关键工程细节。 你如果最近在搭前端项目大概率绕不开这三样东西Vite、Vue3、TypeScript。标题叫“最新完整ViteVue3TypeScript项目实战”我拿到这个压缩包的第一反应是——终于有人把这套组合从零到工程化完整串起来了。过去两三年Vue2升级Vue3、JavaScript切换TypeScript、Webpack迁移Vite每一件事单拎出来都有一堆文章但真正能把这套技术栈融成一个可落地的项目模板而不是零散的知识点拼凑其实没那么多。这篇就当做一个完整的实战复盘从环境准备到项目搭建从核心语法到工程化配置连报错排查一起写出来希望能帮正在学这套组合的人少踩几个坑。1. 技术组合为什么会选它Vite、Vue3、TypeScript的价值拆解1.1 Vite为什么能取代Webpack成为主流启动方式Vite最大的特点就是快而且是那种体验感非常明显的快。传统Webpack构建的机制是先把所有模块打包成一个完整的bundle再启动开发服务器这意味着项目一大冷启动从十几秒到几十秒都是家常便饭。Vite则利用浏览器原生ES Module的能力开发环境下直接按需加载模块浏览器请求哪个文件Vite就编译哪个文件所以冷启动速度基本能控制在1秒左右热更新也是毫秒级。有同学可能觉得快一点点无所谓反正项目能跑就行。但实际开发中这个体验差异会被无限放大。以前改一个组件刷新整个页面要等好几秒Vite只需要精确更新被修改的模块那种密集调试时的顺畅感是完全不同的。Vite内部还做了依赖预构建把项目里用到的第三方依赖比如Vue、Vue Router、Pinia先用esbuild打包成ES Module后续请求直接从缓存里取这也是它快的原因之一。1.2 Vue3组合式API给生产力带来的改变Vue3推出组合式API之后写法的变化远不止是“多了一个setup”这么简单。Vue2时代的Options API逻辑是按data、methods、computed、watch这些选项机械划分的。一个业务组件写到几百行时同一个功能的代码会被拆散到各个选项中要想理清某一条业务线得在文件里来回翻。组合式API的核心是把“同一个业务关注点”的代码集中在一起你可以在setup里面按功能模块来组织逻辑代码的可维护性提升非常明显。和TypeScript结合之后这种优势会更进一步。computed、watch、ref这些API都有完善的泛型类型定义写起来几乎能获得完整的类型提示很多低级错误可以在编译阶段被拦下来。比如你定义了一个refstring后面不小心给他赋值了一个number编译器会直接标红。这种体验在Vue2时代是完全没有的。1.3 TypeScript并不是选做题很多人把TypeScript当成“可选项”觉得JavaScript写得好好的没必要上TS。但从团队协作和项目长期维护的角度看TypeScript更像是一个基础设施。项目规模一大数据结构的约定就变得非常关键。比如接口返回的字段、组件的props、全局状态的数据结构这些如果没有类型约束就只能靠开发人员之间的口头约定一旦换人或者时间长了就可能导致错误的风险很高。TypeScript的价值还体现在代码重构上。当你把一个组件拆分成两个组件或者修改一个公共函数的时候编译器会帮你把所有受影响的地方都标出来你不用担心漏改某个调用方。这些好处在中小型项目中体现得可能不明显但一旦项目进入持续迭代阶段类型系统带来的安全感就会非常明显。2. 环境准备与项目初始化从零搭建完整项目骨架2.1 环境要求和脚手架初始化实战项目的第一步是环境。Node版本建议用16.18以上可以直接用Node官网的LTS版本。如果你的机器上同时有多个Node版本建议用nvm管理方便随时切换。Vite对Node版本的依赖是硬性的版本太低会直接报错这一点在排查问题时可以先排查。脚手架初始化用官方推荐的方式npm create vitelatest my-project -- --template vue-ts这里vue-ts模板会直接生成Vue3 TypeScript Vite的基础项目结构。如果用pnpm命令就是pnpm create vite my-project --template vue-ts后面安装依赖cd my-project npm install npm run dev启动之后访问http://localhost:5173能看到Vite基础页面说明项目骨架已经跑通了。整个初始化过程几十秒比早年Webpack配Vue的项目快太多了。2.2 目录结构与代码规范脚手架默认生成的目录结构是src/components、src/views有些版本没有views需要自己创建实战项目建议在src下增加api、router、store、utils、types、styles等目录按职责划分api统一放接口请求函数router路由配置和守卫storePinia状态模块utils工具函数、请求封装types全局类型定义styles全局样式和变量代码规范方面项目里加入ESLint和Prettier是非常必要的。vue-ts模板默认带了ESLint基础配置但Prettier可能需要手动加npm install -D prettier eslint-config-prettier eslint-plugin-prettier然后在.eslintrc.cjs里扩展plugin:prettier/recommended这样ESLint和Prettier就可以协同工作代码格式统一团队成员之间不用为了缩进和引号争论。提示代码规范这种东西越早定越好。等代码写多了再统一格式每次保存都会触发大面积diff很影响代码审查体验。2.3 tsconfig核心配置与baseUrl弃用说明TypeScript的编译配置是实战中一个重要的环节。新版Vite模板生成的tsconfig.json通常分成tsconfig.app.json和tsconfig.node.json这是为了让服务端和客户端的类型检查范围互相独立。很多同学早期喜欢用baseUrl配置路径别名{ baseUrl: ., paths: { /*: [src/*] } }但TypeScript新版本开始弃用baseUrl这个选项并提示选项baseUrl已弃用将在TypeScript 7.0中停止运行。实际处理起来很简单新版TS允许直接在paths里写相对路径{ compilerOptions: { paths: { /*: [./src/*] } } }这样既满足了路径别名的需求又兼容了新版TS的配置规范。同时在vite.config.ts里也要配置对应的别名否则代码运行时浏览器找不到指向哪里import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, src) } } })配置这一步检查是否生效有个简单的办法在任意组件里写import { ref } from vue和import { useUserStore } from /store/user如果VSCode能正确跳转说明别名配置成功了。3. Vue3核心写法与TypeScript落地姿势3.1 ref与reactive的选型逻辑Vue3的响应式API里ref和reactive是最常用的两个。很多新手一开始会纠结到底用哪个。我的建议很简单尽量用ref少用reactive。原因有几个方面。ref可以包装任意数据类型包括对象和数组而且用的时候是count.value虽然多打几个字但语义非常清晰。reactive只能包装对象而且直接访问属性时不需要.value写起来稍微方便一点但它有一个容易踩坑的地方解构之后响应性会丢失。const state reactive({ count: 0, name: hello }) const { count, name } state // 这里 count 和 name 已经不再是响应式的了除非你真的确定不会解构这个对象否则用ref会更保险。而且ref配合TypeScript的泛型使用也很直观const count refnumber(0) const userList refUserInfo[]([])这样写出来的类型一目了然代码审查的时候也能很快知道每个变量的预期类型。3.2 computed与watch的典型场景computed在Vue3里和Vue2的主要区别是类型推断更强。如果你在computed里返回一个不符合泛型约束的类型编译器会直接报错。实际开发中典型的场景是根据状态计算展示逻辑const totalPrice computed(() { return cartItems.value.reduce((sum, item) sum item.price * item.quantity, 0) })这个totalPrice会被推断为ComputedRefnumber在模板里直接用{{ totalPrice }}即可。watch在组合式API中需要从vue里显式导入用法上多了一个immediate选项来控制是否在初始化时立即执行watch( () route.query.page, (newPage, oldPage) { fetchList({ page: newPage }) }, { immediate: true } )如果你想监听多个数据源传一个数组进去就行watch([page, pageSize], ([newPage, newSize]) { console.log(分页变化了, newPage, newSize) })一个比较实用的技巧是当watch的逻辑和某个数据源的初始化逻辑一致时一定要加immediate: true这样可以省掉在onMounted里重复调用的代码。3.3 defineProps与defineEmits的类型化写法Vue3的defineProps和defineEmits在script setup语法下是编译器宏不需要手动导入而且天然支持TypeScript类型定义。props的写法如下interface UserCardProps { user: UserInfo showAvatar?: boolean role: admin | user | visitor } const props definePropsUserCardProps() // 需要默认值的时候用 withDefaults 包裹 const props withDefaults(definePropsUserCardProps(), { showAvatar: true })这里有个比较容易被忽略的点如果用接口来定义props类型那么props的默认值不能写在接口里必须依靠withDefaults来处理。另外role这种联合类型写法和字符串直接比较不同它提供了编译期的安全检查传错了值会直接标红。emits的类型化写法则是const emit defineEmits{ (e: update:modelValue, value: string): void (e: delete, id: number): void }()在这种写法下触发事件的方式和原来一样emit(update:modelValue, newValue)。好处是参数类型被约束住了传错类型编译器会提示团队协作时每个事件的入参一目了然。3.4 v-model与组件双向绑定的新姿势Vue3的v-model可以用于自定义组件且一个组件支持多个v-model这一块和Vue2差异很大。在Vue2中自定义组件的v-model默认绑定的是value属性和input事件而Vue3改成了modelValue属性和update:modelValue事件。如果有多个值需要双向绑定直接用v-model:keyword、v-model:page这样的写法即可。在子组件里对应的类型定义是这样的const props defineProps{ keyword: string page: number }() const emit defineEmits{ (e: update:keyword, value: string): void (e: update:page, value: number): void }() function handleInput(event: Event) { emit(update:keyword, (event.target as HTMLInputElement).value) }Vue3.4以后的版本还提供了defineModel宏能进一步简化这个写法一行代码就完成双向绑定const keyword defineModelstring(keyword) const page defineModelnumber(page)defineModel返回的是一个ref修改它会自动触发update:keyword事件给父组件。这个API用起来很省事但如果项目里Vue版本低于3.4还是老老实实手动写props和emits。4. 工程化实战路由、状态管理与请求封装4.1 路由配置与组合式API中的守卫写法Vue Router 4是配合Vue3的版本基本用法和Vue Router 3类似用createRouter代替了new VueRouter。createWebHistory对应的是mode: historycreateWebHashHistory对应mode: hash。实战项目里路由配置建议用模块化拆开不要写在一个巨大数组里// src/router/index.ts import { createRouter, createWebHistory } from vue-router import { routes } from ./routes const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes }) router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ name: login }) } else { next() } }) export default router在Vue3的组合式API中Vue2里的beforeRouteEnter组件内守卫写法已经不再推荐。beforeRouteEnter在script setup里没法访问this因为setup阶段组件实例还没创建。替代方案是使用onBeforeRouteLeave和onBeforeRouteUpdate这两个组合式函数它们可以直接在setup里使用。如果需要“进入路由前获取数据”通常直接放在组件初始化逻辑里用onMounted或者配合immediate的watch来做这样逻辑更清晰也更容易被测试覆盖。4.2 Pinia状态管理与持久化Pinia是Vue3官方推荐的状态管理库相比Vuex最直观的变化是更简洁去掉了mutationsstate和actions直接定义就行。实际开发中defineStore的用法如下// src/store/user.ts import { defineStore } from pinia import { loginApi, getUserInfoApi } from /api/user import type { LoginParams, UserInfo } from /types/user export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: {} as UserInfo }), getters: { isLoggedIn: (state) !!state.token }, actions: { async login(params: LoginParams) { const data await loginApi(params) this.token data.token localStorage.setItem(token, data.token) }, async fetchUserInfo() { this.userInfo await getUserInfoApi() }, logout() { this.token this.userInfo {} as UserInfo localStorage.removeItem(token) } } })在组件里使用的时候注意一个细节直接解构store会丢失响应性所以需要用到storeToRefsconst userStore useUserStore() const { token, userInfo } storeToRefs(userStore) const { login, logout } userStorePinia持久化有两种实现方式一种是手动在每个action里同步localStorage另一种是使用pinia-plugin-persistedstate插件。插件方式只需在store里加一行persist: true即可实际用起来省心很多强烈推荐。4.3 axios请求封装与Vite代理配置请求封装是每个Vue项目都绕不开的基础设施。axios封装时要把baseURL、超时时间、请求拦截器、响应拦截器都集中处理。响应拦截器里需要统一处理业务错误码、HTTP状态码异常、Token过期等场景。一个典型的封装结构// src/utils/request.ts import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use( (config) { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, (error) Promise.reject(error) ) service.interceptors.response.use( (response) { const res response.data if (res.code ! 0) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, (error) { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default serviceVite代理配置在vite.config.ts里server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } }这里/api前缀是会传给后端还是被重写掉需要和后端约定。很多项目第一次联调时出现http proxy error多半是因为target地址配错了或者后端服务还没启动。遇到这种问题先确认后端接口是不是真的能通用浏览器直接访问http://localhost:8080/api/xxx试试如果直接访问都不通说明问题在后端而不是代理配置。4.4 项目启动报错ERR_MODULE_NOT_FOUND排查开发过程中经常遇到的一种报错是error [ERR_MODULE_NOT_FOUND]: Cannot find package vite imported from ...这个报错的含义是Node在模块解析时找不到vite这个包。常见的排查路径有几个一是node_modules没安装完整删掉重装rm -rf node_modules package-lock.json npm install二是Node版本和Vite版本不兼容Vite 4要求Node 14.18或16Vite 5要求Node 18。可以用node -v确认版本必要时用nvm use 18切换。三是包管理器混用导致lock文件错乱。比如之前用了npm后来用pnpm安装依赖node_modules的目录结构会不一样。建议一个项目固定一个包管理器不要混用。还有种比较少见的场景是IDE的集成终端没有继承正确的环境变量导致node命令指向了错误的Node版本。这时在终端里手动执行which node确认一下路径或者直接关掉IDE重新打开终端。5. 实际开发中踩过的坑与排查速查表5.1 热搜里的“http proxy error”背后是什么问题很多人会遇到这样的日志[vite] http proxy error: /api/form/list?page1pagesize10 aggregate第一眼看到这个报错会有点慌其实它要表达的信息很直接Vite启动的开发服务器尝试代理/api前缀的请求到目标服务器但请求失败了。aggregate错误通常意味着底层网络连接异常。排查顺序建议是看控制台请求的实际状态码是502、504还是ECONNREFUSED确认target地址是否正确后端服务是否启动确认是否跨域后端是否允许了来自http://localhost:5173的请求如果是HTTPS接口检查代理配置是否需要加secure: false我实际遇到最多的情况就是后端服务启动在了别的端口或者计算机重启之后后端没有自启动。这里没有什么捷径顺着链路一层层看就行。5.2 TypeScript的baseUrl弃用提示该怎么处理最近不少人在更新TypeScript版本后看到了这个提示选项“baseUrl”已弃用并将停止在 TypeScript 7.0 中运行。指定 compilerOptions.paths 时不再需要设置 baseUrl。实际上TypeScript 5.x版本就已经开始不推荐使用baseUrl了。处理方式很简单移除baseUrl配置项把paths里的路径写完整用./src/*替代原来的src/*同时确保jsconfig或tsconfig里的include范围正确如果你的项目恰好升级了TS版本导致这个提示出现不用紧张按上面的方式改完再跑一次vue-tsc类型检查确认没有新的报错就没事了。5.3 组件初始化报错init_runtime_dom_esm_bundler is not define这个报错让人很费解因为它不是一个常规的语法或类型错误而是运行时找不到init_runtime_dom_esm_bundler。这个函数是Vue的运行时模块在初始化DOM渲染器时导出的出现这个报错大概率是Vue相关包的版本或构建配置不一致造成的。实际开发中遇到的情况项目里同时安装了多个Vue版本比如vue和vue-demi都引用了不同版本的Vue使用了低版本Vite和高版本Vue的组合部分依赖预构建缓存过期CDN引入和本地npm包混用解决步骤是npm run build 21 | grep -i vue # 先看构建产物里引用了几个Vue版本 rm -rf node_modules package-lock.json npm install如果还不行把vite.config.ts里的optimizeDeps配置删掉清一下node_modules/.vite目录重新npm run dev。有时候Vite的依赖预构建缓存会留下旧版本的内容清一下就好了。5.4 打包体积优化与代码混淆Vite项目打包产物默认是未压缩的ES Module文件部署前肯定要处理。代码混淆可以用vite-plugin-terser或直接在build配置里设置terserOptionsbuild: { terserOptions: { compress: { drop_console: true, drop_debugger: true }, format: { comments: false } } }这样开发时的console.log和debugger都会被移除文件体积能减小一部分。但如果项目里有需要保留的错误日志可以改用更细粒度的处理在封装好的logger工具里判断环境变量生产环境只过滤低级别日志而不是全部删除。打包体积优化方面路由懒加载是收益最大的一项。把每个页面组件用动态import的方式引入const UserList () import(/views/user/UserList.vue)这样每个路由页面会单独打包成独立chunk首屏只加载当前页面所需的代码而不是把整个项目所有模块一起打包。再配合manualChunks把第三方库vue、vue-router、pinia、element-plus等拆成单独文件可以充分利用浏览器的缓存机制公共库代码不经常变动用户二次访问时可以直接命中缓存。5.5 常见报错速查表实操过程中把典型报错整理成一张速查表遇到问题直接对照查找效率会高很多。报错信息可能原因解决办法Cannot find package vitenode_modules损坏或缺失删除node_modules和lock文件后重装http proxy error: /api/...后端服务未启动或代理地址错误检查target地址、后端端口、服务状态选项“baseUrl”已弃用TypeScript版本升级移除baseUrlpaths改用相对路径init_runtime_dom_esm_bundler is not defineVue包版本冲突或依赖缓存过期清空依赖缓存后重装Cant find module /...路径别名配置不一致同时检查vite.config.ts和tsconfig.jsondefineProps is not definedscript setup未正确配置确认使用