news 2026/10/15 19:33:02

从零搭建 Vue 3 工程化项目:Vite + TypeScript + Pinia + Element Plus 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零搭建 Vue 3 工程化项目:Vite + TypeScript + Pinia + Element Plus 实战

前言

在 收藏向 | 2026 前端开发知识体系:React、Vue、Next.js、Nuxt 全解析 中,我们把前端技术栈拆成了 12 个模块,解决了「有哪些、各管什么」的问题。

但真正动手做项目时,难点往往不是「知不知道 Vite、Pinia」,而是「先装什么、目录怎么分、规范怎么落地、请求怎么封装」。

本篇就沿着第 1 篇的脉络,用 Vue 3 技术栈(Vite + TypeScript + Pinia + Vue Router + Element Plus)串一条完整的落地链路。

读完你应该能独立搭出一个能开发、能规范、能构建的项目骨架,而不是停留在会用某个 API。

本篇技术栈选型如下:

关注点选型理由
构建工具Vite冷启动快、配置轻,Vue 官方推荐
语言TypeScript大型项目可维护性与类型提示
状态管理PiniaVue 官方推荐,组合式 API 友好
路由Vue Router官方路由,支持嵌套与守卫
UI 组件库Element Plus中后台常用,按需引入
HTTP 客户端Axios拦截器机制成熟,统一处理 token 与错误
代码规范ESLint + Prettier + Husky统一风格、提交前检查

工程化搭建全景思维导图

先用一张思维导图展示本篇的完整搭建链路,你可以先扫一眼这张图,建立对工程化脚手架的整体认知:

一、项目初始化

先用 pnpm 创建 Vite + Vue + TS 项目。pnpm 的硬链接机制能节省磁盘空间,也方便后续 monorepo。

pnpmcreate vite my-app--templatevue-tscdmy-apppnpminstallpnpmdev

vue-ts模板已经内置了 Vite、Vue 3、TypeScript 与@vitejs/plugin-vue,开箱即可运行。

接着安装本篇所需的全部依赖:

# 运行时依赖pnpmaddvue-router pinia axios element-plus# 开发依赖pnpmadd-D@types/node unplugin-vue-components unplugin-auto-importpnpmadd-Deslint prettier husky lint-stagedpnpmadd-D@eslint/js typescript-eslint eslint-plugin-vue eslint-plugin-prettier eslint-config-prettier

说明:ESLint 10 已彻底移除.eslintrc.*legacy 配置,必须使用 flat config(eslint.config.mjs),因此需要额外安装@eslint/js和typescript-eslint。

二、目录结构

一个清晰的分层目录是长期维护的基础。建议按职责划分,而不是按类型堆砌。

src/ ── api/ # 接口请求封装 ── assets/ # 静态资源 ├── components/ # 通用组件 ├── composables/ # 组合式函数(useXxx) ├── router/ # 路由配置 ├── stores/ # Pinia 状态 ├── utils/ # 工具函数(request 等) ├── views/ # 页面级组件 ├── App.vue ├── env.d.ts # 环境变量类型声明 ├── main.ts └── style.css # 全局样式

顺手配置路径别名,避免满屏的../../:

// vite.config.tsimport{fileURLToPath,URL}from'node:url'import{defineConfig}from'vite'importvuefrom'@vitejs/plugin-vue'exportdefaultdefineConfig({plugins:[vue()],resolve:{alias:{'@':fileURLToPath(newURL('./src',import.meta.url)),},},})

配置别名后记得在tsconfig.app.json中同步。注意 TypeScript 6 已废弃baseUrl,paths必须使用相对路径:

{"extends":"@vue/tsconfig/tsconfig.dom.json","compilerOptions":{"paths":{"@/*":["./src/*"]}},"include":["src/env.d.ts","src/**/*.ts","src/**/*.vue"]}

踩坑提示:旧教程中常见的"baseUrl": "."写法在 TypeScript 6 中会报TS5101废弃警告,TS 7 将彻底移除。

去掉baseUrl后,paths的值改为"./src/*"即可,TypeScript 会相对于 tsconfig 文件位置解析。

三、TypeScript 类型声明

Vite 通过import.meta.env注入环境变量,但 TypeScript 不知道这些变量的存在,需要手动声明类型。

新建src/env.d.ts:

/// <reference types="vite/client" />interfaceImportMetaEnv{readonlyVITE_APP_TITLE:stringreadonlyVITE_API_BASE_URL:string}interfaceImportMeta{readonlyenv:ImportMetaEnv}

这样在代码中使用import.meta.env.VITE_API_BASE_URL时就有完整的类型提示,写错变量名也会在编译期报错。

四、代码规范落地

规范靠自觉是守不住的,要靠工具强制。ESLint 负责检查、Prettier 负责格式化、Husky + lint-staged 在提交前自动跑一遍。

4.1 Prettier 配置

// .prettierrc.json{"semi":false,"singleQuote":true,"tabWidth":2,"trailingComma":"none"}

4.2 ESLint 10 Flat Config

ESLint 10 不再支持.eslintrc.*,必须使用eslint.config.mjs:

// eslint.config.mjsimportjsfrom'@eslint/js'importtsfrom'typescript-eslint'importvuefrom'eslint-plugin-vue'importprettierfrom'eslint-plugin-prettier/recommended'exportdefault[{ignores:['dist/**','node_modules/**','src/auto-imports.d.ts','src/components.d.ts']},js.configs.recommended,...ts.configs.recommended,...vue.configs['flat/recommended'],prettier,{files:['**/*.vue'],languageOptions:{parserOptions:{parser:ts.parser}}},{rules:{'@typescript-eslint/no-explicit-any':'off','@typescript-eslint/no-unused-vars':['error',{argsIgnorePattern:'^_'}],'vue/multi-word-component-names':'off'}}]

踩坑提示:旧版--ext .vue,.ts参数在 flat config 中已不支持,package.json的 lint 脚本应改为eslint . --fix。

4.3 Husky + lint-staged

pnpmexechusky initecho"npx lint-staged">.husky/pre-commit

在package.json中声明提交前自动格式化的范围:

{"lint-staged":{"*.{js,ts,vue}":["prettier --write","eslint --fix"]}}

这样任何人git commit时,改动的文件都会被自动检查与格式化,脏代码进不了仓库。

五、路由搭建

安装并配置 Vue Router,用createWebHistory启用干净的 URL。

pnpmaddvue-router

路由配置支持嵌套与懒加载,路由组件按需引入可减小首屏体积:

// router/index.tsimport{createRouter,createWebHistory}from'vue-router'constrouter=createRouter({history:createWebHistory(import.meta.env.BASE_URL),routes:[{path:'/login',name:'Login',component:()=>import('@/views/Login.vue')},{path:'/',component:()=>import('@/views/Layout.vue'),children:[{path:'',name:'Home',component:()=>import('@/views/Home.vue')}]}]})exportdefaultrouter

配合全局前置守卫做登录鉴权,这是路由里最实用的能力:

// router/index.ts(追加)router.beforeEach((to)=>{consttoken=localStorage.getItem('token')if(to.meta.requiresAuth&&!token){return{path:'/login'}}})

说明:import.meta.env.BASE_URL是 Vite 内置变量,代表应用的部署基础路径,默认值为/。

部署到子路径时(如https://example.com/app/),只需在 Vite 配置中设置base: '/app/',路由会自动适配。

六、状态管理(Pinia)

安装 Pinia 并在入口注册:

pnpmaddpinia
// main.tsimport{createApp}from'vue'import{createPinia}from'pinia'importElementPlusfrom'element-plus'import'element-plus/dist/index.css'importrouterfrom'./router'importAppfrom'./App.vue'import'./style.css'constapp=createApp(App)app.use(createPinia())app.use(router)app.use(ElementPlus)app.mount('#app')

注意:Element Plus 的 CSS 必须在main.ts中显式引入,否则组件样式不会生效。

用组合式写法定义 Store,类型推导更友好:

// stores/user.tsimport{defineStore}from'pinia'import{ref}from'vue'import{userApi}from'@/api/user'exportconstuseUserStore=defineStore('user',()=>{consttoken=ref(localStorage.getItem('token')||'')constuserInfo=ref<{name:string}|null>(null)asyncfunctionlogin(username:string,password:string){constres=awaituserApi.login({username,password})token.value=res.token localStorage.setItem('token',res.token)}asyncfunctionfetchUserInfo(){userInfo.value=awaituserApi.getUserInfo()}functionlogout(){token.value=''userInfo.value=nulllocalStorage.removeItem('token')}return{token,userInfo,login,fetchUserInfo,logout}})

组件中直接消费:

<script setup lang="ts"> import { useRouter } from 'vue-router' import { useUserStore } from '@/stores/user' const router = useRouter() const userStore = useUserStore() function handleLogout() { userStore.logout() router.replace('/login') } </script> <template> <div> <h1>首页</h1> <el-button @click="handleLogout">退出登录</el-button> </div> </template>

七、数据请求封装

请求层的价值在于统一处理 baseURL、token、错误与登录失效,而不是每处都手写 fetch。

pnpmaddaxios
// utils/request.tsimportaxios,{typeAxiosInstance,typeAxiosRequestConfig,typeAxiosResponse}from'axios'import{ElMessage}from'element-plus'importrouterfrom'@/router'constservice:AxiosInstance=axios.create({baseURL:import.meta.env.VITE_API_BASE_URL,timeout:10000})// 请求拦截:统一带上 tokenservice.interceptors.request.use((config)=>{consttoken=localStorage.getItem('token')if(token&&config.headers){config.headers.Authorization=`Bearer${token}`}returnconfig})// 响应拦截:统一处理错误与登录失效service.interceptors.response.use((response:AxiosResponse)=>{returnresponse.data},(error)=>{if(error.response?.status===401){localStorage.removeItem('token')router.replace('/login')}ElMessage.error(error.message||'请求失败')returnPromise.reject(error)})exportfunctionget<T=any>(url:string,config?:AxiosRequestConfig):Promise<T>{returnservice.get(url,config)asPromise<T>}exportfunctionpost<T=any>(url:string,data?:any,config?:AxiosRequestConfig):Promise<T>{returnservice.post(url,data,config)asPromise<T>}

踩坑提示 1:响应拦截器返回了response.data,但 TypeScript 认为service.get()返回的是Promise<AxiosResponse<T>>,拦截器的类型变换不会被类型系统自动感知。需要在get/post函数中加as Promise<T>断言。

踩坑提示 2:不要在请求拦截器里调用useUserStore()。Axios 拦截器在模块加载时就会注册,而 Pinia Store 需要等app.use(createPinia())之后才能使用,在拦截器里调用会导致「Pinia 未激活」的运行时错误。直接从localStorage读取 token 是最稳妥的做法。

业务接口只关心「调哪个接口、拿什么数据」:

// api/user.tsimport{get,post}from'@/utils/request'exportinterfaceLoginParams{username:stringpassword:string}exportinterfaceUserInfo{id:numbername:stringrole:string}exportconstuserApi={login:(data:LoginParams)=>post<{token:string}>('/user/login',data),getUserInfo:()=>get<UserInfo>('/user/info')}

八、UI 方案与按需引入

Element Plus 配合unplugin-vue-components实现按需自动引入,省去手动 import 组件:

pnpmaddelement-pluspnpmadd-Dunplugin-vue-components unplugin-auto-import
// vite.config.tsimport{fileURLToPath,URL}from'node:url'import{defineConfig}from'vite'importvuefrom'@vitejs/plugin-vue'importAutoImportfrom'unplugin-auto-import/vite'importComponentsfrom'unplugin-vue-components/vite'import{ElementPlusResolver}from'unplugin-vue-components/resolvers'exportdefaultdefineConfig({plugins:[vue(),AutoImport({resolvers:[ElementPlusResolver()],imports:['vue','vue-router','pinia'],dts:'src/auto-imports.d.ts'}),Components({resolvers:[ElementPlusResolver()],dts:'src/components.d.ts'})],resolve:{alias:{'@':fileURLToPath(newURL('./src',import.meta.url))}},server:{proxy:{'/api':{target:'http://localhost:3000',changeOrigin:true}}}})

之后模板里可直接使用<el-table>、<el-button>,无需手动注册:

<template> <el-table :data="users"> <el-table-column prop="name" label="姓名" /> <el-table-column prop="email" label="邮箱" /> </el-table> </template> <script setup lang="ts"> import { ref } from 'vue' import { userApi } from '@/api/user' const users = ref<UserInfo[]>([]) userApi.getUserList({ page: 1, size: 10 }).then((res) => { users.value = res.list }) </script>

说明:unplugin-auto-import的imports: ['vue', 'vue-router', 'pinia']配置会自动把ref、computed、useRouter、defineStore等 API 注入为全局可用,无需在每个文件中手动 import。

生成的类型声明文件src/auto-imports.d.ts和src/components.d.ts应加入.gitignore或提交到仓库(推荐提交,保证 CI 环境也能获得类型提示)。

九、环境变量与开发代理

不同环境的接口地址、功能开关用环境变量区分,避免把测试地址带到生产。

# .env.developmentVITE_APP_TITLE=前端项目实战手册VITE_API_BASE_URL=/api# .env.productionVITE_APP_TITLE=前端项目实战手册VITE_API_BASE_URL=https://api.example.com

注意:开发环境中VITE_API_BASE_URL设为/api是相对路径,需要配合 Vite 的server.proxy把请求转发到后端服务(见第八节 vite.config.ts 中的server.proxy配置)。生产环境则直接填写完整的后端地址。

代码里通过import.meta.env.VITE_API_BASE_URL读取(见第七节)。构建时 Vite 会按模式注入对应变量:

pnpmbuildpnpmpreview

pnpm preview用生产构建产物本地预览,是上线前验证构建结果是否正常的最后一道关口。

本篇小结

到这里,一个 Vue 3 项目的工程骨架已经成型:

  • 用 pnpm + Vite 完成初始化与别名配置
  • 用@vue/tsconfig+ 相对路径paths配置 TypeScript(兼容 TS 6)
  • 用env.d.ts声明环境变量类型
  • 用 ESLint 10 flat config + Husky 强制代码规范
  • 用 Vue Router 搭建路由与鉴权守卫
  • 用 Pinia 组合式 API 管理全局状态
  • 用 Axios 拦截器统一封装请求(避免在拦截器中调用 Store)
  • 用 Element Plus + unplugin 按需引入提升开发效率
  • 用环境变量 + Vite proxy 隔离多环境配置

这套骨架解决的是「能不能持续开发」的问题。但代码写完只是开始,怎么把它安全、稳定地发到线上,才是很多前端团队的短板,这里挖个坑,下一次再说这个问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 19:32:51

LangChain 源码入门 04:Runnable 如何统一调用、批处理、异步与流式接口

1. 一个组件要具备哪些行为&#xff1f; 最简模型是&#xff1a;输入 Input → 处理 → 输出 Output。如果每种组件都各自定义同步、异步、批量、流式的方法名&#xff0c;组合起来会很困难。Runnable 对这些执行方式提供统一词汇&#xff1a; 操作用途invoke(x)处理一份输入…

作者头像 李华
网站建设 2026/10/15 19:32:43

IntelliJ IDEA行注释空行空格问题解析与配置解决方案

1. 先别急着调设置&#xff1a;搞清楚注释时的空格是从哪冒出来的我最早被这个问题恶心到&#xff0c;是在一次大范围代码重构的时候。方法内部为了分层清晰&#xff0c;逻辑块之间留了不少空行。某天想临时注释掉一整段代码&#xff0c;习惯性Ctrl /按下去&#xff0c;结果满…

作者头像 李华
网站建设 2026/10/15 19:32:07

Antd Vue 4 搭建避坑指南:Vite 脚手架+配置校验全解析

1. 为什么“AntdVue”搭建流程值得单独拆成上篇&#xff1f;——从三个真实卡点说起我带过不少刚转 Vue 的前端新人&#xff0c;也帮某高校实验室的跨平台系统做过技术选型支持。每次聊到 Ant Design Vue&#xff08;下文简称 Antd Vue&#xff09;&#xff0c;总有人一上来就问…

作者头像 李华
网站建设 2026/10/15 19:30:29

MedPlanning 2.0 全新升级|免费开放|从影像到手术规划

MedPlanning 影像规划助手迎来重磅更新&#xff01;在 2.0 版本基础上&#xff0c;我们全面升级了数据管理、手术规划、可视化三大核心能力&#xff0c;让骨科手术从影像导入到方案导出&#xff0c;再到移动端二维码和AR 查看&#xff0c;实现全流程高效闭环。✨ 核心升级亮点 …

作者头像 李华
网站建设 2026/10/15 19:29:29

2026电赛A题复现先导课:PLECS仿真、PFC控制方案与硬件联调路线

2026电赛A题复现先导课&#xff1a;PLECS仿真、PFC控制方案与硬件联调路线 做电源类赛题&#xff0c;需要把题目要求、电路结构、控制算法和测量结果对应起来。模型中的反馈信号要能对应实际采样点&#xff0c;控制器要能按固定周期在单片机上执行&#xff0c;调参后的变化也要…

作者头像 李华
网站建设 2026/10/15 19:29:07

除湿机技术科普 · 硬核拆解

除湿机的「感知—决策—执行」&#xff1a;温湿度传感响应曲线、PID 控湿逻辑与冷凝结霜自动化霜机制硬核拆解&#xff5c;拒绝小白百科&#xff0c;从物理方程和算法结构讲起一台除湿机的自动控制&#xff0c;本质是三个环节的接力感知把空气的真实状态变成电信号&#xff0c;…

作者头像 李华