Vue3 + Vite + Pinia + Router 实战总结
从 main.js 到组件通信,一次把 Vue3 前端工程的主流开发范式过一遍。
Vue3 + Vite + Pinia + Router 实战总结
学完后端就开始学前端了,说实话一开始挺懵的。
以前写的网页是 jQuery 那一套,DOM 随便操作,数据直接绑页面上。
换到 Vue3 之后,一切都变了,组件化、响应式、路由管理、状态管理,概念一堆。
但项目做完回头再看,发现其实核心逻辑就几块:
项目结构、路由、状态管理、API 封装、页面组件。
把这五块搞清楚,Vue3 项目就能写了。
先说说这套技术栈是什么
我们这个项目用的是目前主流的中后台开发组合:
| 技术 | 作用 |
|---|---|
| Vite | 构建工具,秒级启动,热更新 |
| Vue 3 | 框架核心,组件化开发 |
| Element Plus | UI 组件库,按钮、表格、表单全有 |
| Vue Router | 路由管理,页面无刷新切换 |
| Pinia | 状态管理,跨组件共享数据 |
| Axios | HTTP 请求客户端 |
<script setup> |
组合式 API 语法糖 |
光看名字可能觉得挺多的,但每个东西都有明确的职责,不会乱。
项目结构长什么样
一个标准的 Vue3 项目,目录结构大概是这样的:
src/
├── api/ # API 接口封装
│ ├── http.js # Axios 实例 + 拦截器
│ ├── auth.js # 登录相关接口
│ ├── emp.js # 员工相关接口
│ └── dept.js # 部门相关接口
├── router/ # 路由配置
│ └── index.js
├── stores/ # Pinia 状态管理
│ ├── user.js
│ └── emp.js
├── views/ # 页面组件
│ ├── login/
│ │ └── Login.vue
│ ├── layout/
│ │ ├── Layout.vue
│ │ └── Dashboard.vue
│ └── emp/
│ └── Emp.vue
├── App.vue # 根容器
├── main.js # 应用入口
└── index.html # HTML 模板
每个目录负责单一职责,修改一处不会影响其他地方。
main.js:应用启动入口
main.js 是应用的起点,做的事情不多,就是创建 Vue 实例、注册插件、挂载到 DOM。
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')
就这几行,顺序是固定的:先创建应用,再注册插件,最后挂载。
App.vue:最简根容器
App.vue 做的事最少,只有一个 <router-view>:
<template>
<div id="app">
<router-view />
</div>
</template>
它只负责渲染当前路由对应的组件,不含任何业务逻辑。
路由:页面切换的核心
路由配置在 router/index.js 里,用 Vue Router 4 实现单页面应用的路由管理。
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [
{
path: '/login',
component: () => import('../views/login/Login.vue')
},
{
path: '/',
component: () => import('../views/layout/Layout.vue'),
children: [
{ path: 'dashboard', component: () => import('../views/layout/Dashboard.vue') },
{ path: 'emp', component: () => import('../views/emp/Emp.vue') },
{ path: 'dept', component: () => import('../views/dept/Dept.vue') }
]
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
// 路由守卫:未登录跳转登录页
router.beforeEach((to, from, next) => {
if (to.path !== '/login' && !localStorage.getItem('token')) {
next('/login')
} else {
next()
}
})
export default router
几个关键点:
- 懒加载:
() => import(...)按需加载,减小首屏体积 - 嵌套路由:Layout 作为父路由,子路由在内部渲染
- 路由守卫:
beforeEach做权限校验
状态管理:Pinia 怎么用
Pinia 用来管理跨组件共享的状态,比如用户信息、登录态、列表数据等。
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || null,
userInfo: null
}),
actions: {
async login(username, password) {
// 调用登录接口
const result = await loginApi({ username, password })
this.token = result.token
this.userInfo = { id: result.id, name: result.name }
localStorage.setItem('token', this.token)
localStorage.setItem('userInfo', JSON.stringify(this.userInfo))
},
logout() {
this.token = null
this.userInfo = null
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
}
}
})
在组件里使用:
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
async function handleLogin() {
await userStore.login(form.username, form.password)
router.push('/')
}
</script>
注意是调用函数 useUserStore(),不是 new。
API 封装:Axios 拦截器
所有 HTTP 请求都经过 api/http.js 里的 Axios 实例,统一处理 Token 注入和错误提示。
import axios from 'axios'
import { ElMessage } from 'element-plus'
const api = axios.create({
baseURL: '/api',
timeout: 10000
})
// 请求拦截器:自动注入 Token
api.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) config.headers.token = token
return config
})
// 响应拦截器:统一处理错误
api.interceptors.response.use(
response => {
const { code, msg, data } = response.data
if (code === 1) return data
ElMessage.error(msg || '操作失败')
return Promise.reject(new Error(msg))
},
error => {
if (error.response?.status === 401) {
ElMessage.warning('请先登录')
setTimeout(() => location.href = '/login', 1500)
} else {
ElMessage.error(error.message || '系统异常')
}
return Promise.reject(error)
}
)
export default api
这样写的好处是:
- 所有请求自动带 Token,不用每个组件都手动加
- 错误统一处理,页面不需要重复写错误逻辑
- 401 自动跳登录,体验更流畅
组件写法:<script setup>
Vue3 推荐用 Composition API 的 <script setup> 语法:
<script setup>
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { empApi } from '@/api/emp'
const userStore = useUserStore()
const empList = ref([])
const loading = ref(false)
async function loadData() {
loading.value = true
try {
empList.value = await empApi.list()
} finally {
loading.value = false
}
}
onMounted(loadData)
</script>
<template>
<el-table :data="empList" v-loading="loading">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="phone" label="手机号" />
</el-table>
</template>
ref 声明响应式数据,onMounted 在组件挂载时执行,代码比 Options API 简洁很多。
完整请求流程:从点击到渲染
以「查询员工列表」为例,整个流程是这样的:
用户点击查询按钮
↓
组件调用 store.action(如 empStore.list())
↓
Store 调用 API(empApi.list())
↓
Axios 请求拦截器注入 Token,发出请求
↓
后端返回数据
↓
Axios 响应拦截器处理成功/失败
↓
Store 更新 state(empStore.list = result)
↓
Vue 响应式系统自动触发组件重新渲染
数据变了,界面自动更新,不需要手动操作 DOM。
这就是 Vue3 响应式系统的核心。
一句话总结
Vue3 前端工程的核心就这几件事:
main.js 创建应用 → router 管理路由 → stores 管理状态 → api 封装请求 → views 写组件。
各层职责清晰,修改一处影响范围可控。
刚开始学的时候觉得东西多,写完一个项目之后发现也就那几块,套路固定,照着写就行。