Vue 3 响应式解构、Axios 封装、路由与 Pinia 状态管理实践指南
作者:顾风jy
Vue 3 工程实践里这几个点正好是项目落地最常用的组合。先说结论:响应式解构推荐用 storeToRefs 和 toRefs,Axios 封装重点在拦截器,路由要会用动态参数和守卫,Pinia 选 Setup Store 写法更贴合组合式 API。下面按这几个模块拆开讲
一、toRefs 与 toRef
reactive() 返回一个响应式代理对象。直接解构代理对象时,解构出来的是普通值,响应式连接会丢失;toRefs() 和 toRef() 可以把对象属性转换为与原对象保持同步的 ref。
import { reactive, toRefs, toRef } from 'vue'
const person = reactive({ name: 'lqz', age: 19 })
// 直接解构会失去响应式连接。
const { name: plainName } = person
// toRefs 为对象的每个属性创建 ref,并保持双向同步。
const { name, age } = toRefs(person)
age.value++
console.log(person.age) // 20
// 只需要一个属性时使用 toRef。
const personName = toRef(person, 'name')
personName.value = '新名字'
console.log(person.name)ref(person.name) 只是用当前值创建一个新的 ref,之后与 person.name 没有自动关联;toRef(person, 'name') 才是对原对象属性的引用。组合式函数返回响应式对象时,常使用 toRefs 让调用方可以安全解构。
<script setup>
import { reactive, toRefs } from 'vue'
const person = reactive({ name: 'lqz', age: 19 })
const { name, age } = toRefs(person)
function increaseAge() {
age.value++
}
</script>
<template>
<p>{{ name }},{{ age }} 岁</p>
<button @click="increaseAge">增加年龄</button>
</template>二、axios 基本使用
axios 是基于 Promise 的 HTTP 客户端。它的响应对象通常包含 data、status、headers 等信息,后端真正返回的业务数据一般位于 response.data。
npm install axios
1. 请求电影列表
<script setup>
import { reactive, ref, onMounted } from 'vue'
import axios from 'axios'
const films = reactive([])
const loading = ref(false)
const error = ref('')
async function loadFilms() {
loading.value = true
error.value = ''
try {
const { data } = await axios.get('http://127.0.0.1:8000/films/films/')
// 假设接口格式为 { code: 0, results: [] }。
if (data.code !== 0) throw new Error(data.msg || '接口返回失败')
films.splice(0, films.length, ...data.results)
} catch (err) {
console.error(err)
error.value = '电影数据加载失败'
} finally {
loading.value = false
}
}
onMounted(loadFilms)
</script>
<template>
<p v-if="loading">加载中...</p>
<p v-else-if="error">{{ error }}</p>
<ul v-else>
<li v-for="film in films" :key="film.id">
<h2>{{ film.name }}</h2>
<img :src="film.poster" :alt="film.name" width="200">
<p>{{ film.synopsis }}</p>
</li>
</ul>
</template>当响应式数组使用 reactive([]) 时,可以用 splice 原地更新,也可以直接重新赋值给 ref([])。如果使用 reactive({ results: [] }),则可以用 Object.assign(state, data) 更新对象的多个属性;不要把数组当成普通对象使用 Object.assign 后期待所有场景都清晰可读。
2. axios 配置项
常用配置可以分成请求地址、请求数据和请求行为三类:
axios({
baseURL: 'http://127.0.0.1:8000/api',
url: '/films/',
method: 'get',
params: { page: 1, size: 10 }, // 拼接到 URL 的查询参数
headers: { Authorization: 'Bearer token' },
timeout: 10000
})
axios.post('/login/', {
username: 'lqz',
password: 'secret'
})params 用于查询字符串,data 用于 POST、PUT、PATCH 等请求体。timeout 可以防止请求无限等待,baseURL 适合统一管理后端 API 前缀。
三、axios 实例与拦截器
项目中通常创建一个 axios 实例,将基础地址、超时、认证请求头和错误处理集中管理。请求拦截器在请求发出前执行,响应拦截器在 then 之前执行。
// src/utils/request.js
import axios from 'axios'
const request = axios.create({
baseURL: 'http://127.0.0.1:8000/api/v1',
timeout: 10000
})
request.interceptors.request.use(config => {
const token = localStorage.getItem('access')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
}, error => Promise.reject(error))
request.interceptors.response.use(response => {
// 是否直接返回 data,取决于项目接口规范。
return response.data
}, error => {
if (error.response?.status === 401) {
localStorage.removeItem('access')
// 可以在这里触发路由跳转或统一提示。
}
return Promise.reject(error)
})
export default request拦截器应保持职责单一,不要在其中放置复杂业务逻辑。给请求额外添加自定义请求头可能触发 CORS 预检请求,后端需要允许对应的请求方法和请求头。
四、Promise、async 与 await
Promise 表示一个未来才会完成的异步操作,有 pending、fulfilled 和 rejected 三种状态。状态一旦从 pending 变为成功或失败,就不会再次改变。
const task = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true
if (success) resolve('任务完成')
else reject(new Error('任务失败'))
}, 1000)
})
task
.then(value => console.log(value))
.catch(error => console.error(error))
.finally(() => console.log('任务结束'))Promise 的回调不会阻塞浏览器主线程。then 处理成功值,catch 处理异常;在链式调用中,每个 then 的返回值都会成为下一个 then 的输入。
async 函数总是返回 Promise,await 会等待右侧 Promise 的结果,只能在 async 函数中使用(现代环境也支持顶层 await 的特定场景)。
async function loadUser() {
try {
const response = await request.get('/user/')
console.log(response)
return response
} catch (error) {
console.error('请求失败', error)
throw error
}
}多个互不依赖的请求不必串行等待,可以使用 Promise.all 并行发起:
const [user, settings] = await Promise.all([
request.get('/user/'),
request.get('/settings/')
])
只有后一个请求依赖前一个结果时,才按顺序写多个 await。这样可以减少不必要的等待,也能让错误边界更清晰。
五、Vue Router 4
Vue 3 使用 Vue Router 4。安装后通过 createRouter 和 createWebHistory 创建路由实例,再通过 app.use(router) 注册。
npm install vue-router@4
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '@/views/HomeView.vue'
import LoginView from '@/views/LoginView.vue'
const routes = [
{ path: '/', name: 'home', component: HomeView },
{ path: '/login', name: 'login', component: LoginView },
{
path: '/about/:id',
name: 'about',
component: () => import('@/views/AboutView.vue')
}
]
export default createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')组件模板中使用 <router-link> 进行声明式导航,页面组件显示在 <router-view> 中。JavaScript 中使用 useRouter() 获取路由实例,使用 useRoute() 获取当前路由对象。
<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
function goAbout() {
router.push({
name: 'about',
params: { id: 10 },
query: { from: 'home' }
})
}
</script>
<template>
<router-link to="/">首页</router-link>
<button @click="goAbout">查看详情</button>
<p>当前路径:{{ route.path }}</p>
</template>push 会新增历史记录,replace 会替换当前记录,back 和 go(-1) 可以返回。路径参数使用 route.params 获取,查询参数使用 route.query 获取。
六、Vue Router 4 路由守卫
Vue Router 4 的全局守卫可以直接返回结果,不再强制使用 Vue Router 3 的 next 回调。守卫适合做登录状态检查,但接口权限仍必须由后端验证。
router.beforeEach(to => {
const token = localStorage.getItem('access')
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
if (requiresAuth && !token) {
return {
name: 'login',
query: { redirect: to.fullPath }
}
}
if (to.name === 'login' && token) {
return { name: 'home' }
}
return true
})路由通过 meta.requiresAuth 标记是否需要登录:
{
path: '/admin',
component: () => import('@/views/AdminView.vue'),
meta: { requiresAuth: true }
}要避免登录页本身被守卫重定向造成死循环。登录成功后读取 redirect 时,应确认它是本站路径,避免开放重定向。
七、Pinia 状态管理
Pinia 是 Vue 3 官方推荐的状态管理库。相比 Vuex,它的 API 更简洁,不需要强制区分 mutations 和 actions,支持更好的 TypeScript 推导,也可以按 store 拆分业务模块。
npm install pinia
// src/stores/user.js
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
const username = ref('张三')
const age = ref(19)
const hobbies = ref(['阅读', '音乐'])
const hobbyCount = computed(() => hobbies.value.length)
function increaseAge() {
age.value++
}
return { username, age, hobbies, hobbyCount, increaseAge }
})// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
createApp(App)
.use(createPinia())
.mount('#app')<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
</script>
<template>
<p>{{ userStore.username }}:{{ userStore.age }} 岁</p>
<p>爱好数量:{{ userStore.hobbyCount }}</p>
<button @click="userStore.increaseAge">增加年龄</button>
</template>只有跨组件或跨页面共享的数据才值得放入 Pinia。局部表单输入和临时显示状态放在组件内部,避免 store 变成无边界的全局变量集合。
八、Element Plus
Element Plus 是 Vue 3 对应的桌面端组件库;Vue 2 应使用 Element UI。安装后在入口文件注册,可以使用按钮、表格、表单、弹窗等组件。
npm install element-plus
// main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
createApp(App)
.use(ElementPlus)
.mount('#app')<template>
<el-button type="primary" @click="showMessage">保存</el-button>
</template>
<script setup>
import { ElMessage } from 'element-plus'
function showMessage() {
ElMessage.success('保存成功')
}
</script>组件库可以提高开发效率,但仍应统一表单校验、错误提示和主题规范。大型项目可根据文档使用按需引入,减少最终打包体积。
九、总结
toRefs适合解构整个响应式对象,toRef适合保持单个属性的响应式连接。- axios 实例和拦截器可以统一基础地址、token、超时和错误处理;
async/await让异步流程更清晰。 - Vue Router 4 使用
createRouter和组合式 API,路由守卫可以控制前端导航,但不能替代后端权限。 - Pinia 是 Vue 3 推荐的状态管理库,按业务拆分 store,避免把所有状态都放到全局。
- Element Plus 与 Vue 3 配套,Element UI 与 Vue 2 配套,选择组件库时要先确认项目版本。
到此这篇关于Vue 3 响应式解构、Axios 封装、路由与 Pinia 状态管理实践指南的文章就介绍到这了,更多相关Vue 响应式解构与 Pinia 状态管理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
