1. Vue3 项目第二天实战要点解析
今天继续我们的Vue3实战系列,主要聚焦于项目核心架构的搭建和基础功能的实现。经过第一天的环境准备和项目初始化,现在我们已经有了一个干净的Vue3项目骨架,接下来要开始往里面填充血肉了。
在真实的项目开发中,第二天通常是最关键的架构定型期。我们需要完成路由配置、状态管理初始化、API服务封装等基础建设,这些工作将直接影响后续的开发效率和项目可维护性。我见过太多项目因为前期架构设计不合理,导致后期难以扩展和维护,所以今天的内容虽然基础,但非常重要。
2. 项目架构设计与核心模块搭建
2.1 路由系统配置与优化
首先我们来配置项目路由。Vue Router 4.x版本针对Vue3做了全面优化,使用方式也有了一些变化:
javascript复制// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home,
meta: {
requiresAuth: true // 路由元信息,用于权限控制
}
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue') // 路由懒加载
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
// 全局路由守卫
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// 这里可以添加权限验证逻辑
next()
} else {
next()
}
})
export default router
几个关键点需要注意:
- 使用
createRouter替代原来的new Router - 路由模式从
mode: 'history'改为createWebHistory - 组件导入方式支持直接使用
import()实现懒加载 - 路由守卫的用法基本保持不变
提示:在大型项目中,建议将路由配置拆分为多个模块,然后在主路由文件中合并,这样更易于维护。
2.2 状态管理方案选型与实现
对于状态管理,Vue3提供了多种选择。对于中小型项目,可以使用reactive或ref创建响应式对象;对于大型项目,建议使用Pinia(Vuex的替代方案)。
这里我们使用Pinia作为状态管理方案:
javascript复制// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
user: null
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
async fetchUser(userId) {
const user = await api.fetchUser(userId)
this.user = user
}
}
})
然后在main.js中安装Pinia:
javascript复制import { createPinia } from 'pinia'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
Pinia相比Vuex的优势:
- 更简单的API,去除了mutations概念
- 完整的TypeScript支持
- 模块化设计,不需要嵌套模块
- 更轻量,压缩后只有1KB左右
2.3 API服务层封装
良好的API封装能让前端代码更清晰,也便于统一处理错误和请求拦截。我们使用axios来封装HTTP请求:
javascript复制// api/index.js
import axios from 'axios'
const service = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL,
timeout: 10000
})
// 请求拦截器
service.interceptors.request.use(
config => {
// 在这里可以统一添加token等操作
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
return response.data
},
error => {
// 统一处理错误
if (error.response.status === 401) {
// 处理未授权错误
}
return Promise.reject(error)
}
)
export default service
然后针对具体业务模块创建API文件:
javascript复制// api/user.js
import request from './index'
export function login(data) {
return request({
url: '/auth/login',
method: 'post',
data
})
}
export function getUserInfo() {
return request({
url: '/user/info',
method: 'get'
})
}
3. 核心功能模块开发
3.1 登录功能实现
登录是大多数系统的第一个功能模块,我们来实现一个完整的登录流程:
vue复制<template>
<div class="login-container">
<el-form :model="loginForm" :rules="loginRules" ref="loginFormRef">
<el-form-item prop="username">
<el-input v-model="loginForm.username" placeholder="用户名"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="密码"></el-input>
</el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
</el-form>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { login } from '@/api/user'
const router = useRouter()
const userStore = useUserStore()
const loginForm = ref({
username: '',
password: ''
})
const loginRules = ref({
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
})
const handleLogin = async () => {
try {
const { data } = await login(loginForm.value)
userStore.setToken(data.token)
router.push('/')
} catch (error) {
console.error('登录失败:', error)
}
}
</script>
关键点说明:
- 使用
<script setup>语法,这是Vue3的组合式API的编译时语法糖 - 表单验证使用Element Plus的Form组件
- 登录成功后,将token存储到Pinia和localStorage中
- 使用try-catch处理异步请求错误
3.2 权限控制实现
基于路由的权限控制是后台系统的常见需求,我们扩展之前的路由守卫:
javascript复制router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
// 如果路由需要认证
if (to.matched.some(record => record.meta.requiresAuth)) {
if (userStore.token) {
// 如果用户信息不存在,则获取用户信息
if (!userStore.user) {
try {
await userStore.fetchUserInfo()
next()
} catch (error) {
// 获取用户信息失败,清除token并跳转到登录页
userStore.resetToken()
next(`/login?redirect=${to.path}`)
}
} else {
next()
}
} else {
next(`/login?redirect=${to.path}`)
}
} else {
next()
}
})
3.3 全局组件自动注册
在项目中,我们通常会有一些全局公共组件。手动注册每个组件很麻烦,我们可以实现自动注册:
javascript复制// main.js
const app = createApp(App)
const modules = import.meta.glob('./components/global/*.vue')
for (const path in modules) {
const componentName = path
.split('/')
.pop()
.replace(/\.\w+$/, '')
app.component(componentName, defineAsyncComponent(modules[path]))
}
这样,只要把组件放在src/components/global目录下,就会自动注册为全局组件,无需手动导入。
4. 开发效率优化技巧
4.1 配置路径别名
在vite.config.js中配置路径别名,可以避免繁琐的相对路径:
javascript复制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'),
'components': path.resolve(__dirname, './src/components')
}
}
})
同时需要在jsconfig.json或tsconfig.json中添加:
json复制{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"components/*": ["src/components/*"]
}
}
}
4.2 环境变量配置
Vue3项目使用Vite作为构建工具,环境变量需要以VITE_开头:
env复制# .env.development
VITE_API_BASE_URL=http://dev.api.example.com
env复制# .env.production
VITE_API_BASE_URL=http://api.example.com
在代码中通过import.meta.env.VITE_API_BASE_URL访问。
4.3 代码风格统一
配置ESLint和Prettier保证代码风格一致:
javascript复制// .eslintrc.js
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/typescript/recommended',
'@vue/prettier',
'@vue/prettier/@typescript-eslint'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/multi-word-component-names': 'off'
}
}
5. 常见问题与解决方案
5.1 响应式丢失问题
在Vue3中,解构props会导致响应式丢失:
javascript复制// 错误做法
const { title } = defineProps(['title'])
console.log(title) // 不是响应式的
// 正确做法
const props = defineProps(['title'])
console.log(props.title) // 保持响应式
或者使用toRefs:
javascript复制const props = defineProps(['title'])
const { title } = toRefs(props)
console.log(title.value) // 保持响应式
5.2 组件通信方式选择
Vue3中组件通信有多种方式,根据场景选择:
- Props/Events:父子组件通信
- provide/inject:跨层级组件通信
- Pinia/Vuex:全局状态管理
- Event Bus:使用mitt库实现事件总线(适用于简单场景)
5.3 生命周期钩子变化
Vue3的组合式API中,生命周期钩子有所变化:
javascript复制import { onMounted, onUpdated, onUnmounted } from 'vue'
setup() {
onMounted(() => {
console.log('组件挂载')
})
onUpdated(() => {
console.log('组件更新')
})
onUnmounted(() => {
console.log('组件卸载')
})
}
与选项式API的对应关系:
- beforeCreate -> 使用setup()
- created -> 使用setup()
- beforeMount -> onBeforeMount
- mounted -> onMounted
- beforeUpdate -> onBeforeUpdate
- updated -> onUpdated
- beforeUnmount -> onBeforeUnmount
- unmounted -> onUnmounted
5.4 样式作用域问题
在Vue单文件组件中,可以使用scoped属性限制样式作用域:
vue复制<style scoped>
/* 这里的样式只作用于当前组件 */
</style>
但在Vue3中,子组件的根元素会同时受父组件和子组件的scoped样式影响。如果不想让父组件的样式影响子组件,可以使用:
vue复制<style>
/* 全局样式 */
</style>
<style scoped>
/* 组件样式 */
</style>
或者使用CSS Modules:
vue复制<template>
<div :class="$style.red">文字</div>
</template>
<style module>
.red {
color: red;
}
</style>
6. 项目结构与代码组织建议
经过第二天的开发,我们的项目结构应该大致如下:
code复制src/
├── api/ # API请求封装
│ ├── index.js # axios实例和拦截器
│ └── user.js # 用户相关API
├── assets/ # 静态资源
├── components/ # 公共组件
│ └── global/ # 全局自动注册组件
├── router/ # 路由配置
│ └── index.js
├── stores/ # Pinia状态管理
│ └── user.js # 用户状态
├── utils/ # 工具函数
├── views/ # 页面组件
│ ├── Home.vue
│ └── Login.vue
├── App.vue # 根组件
└── main.js # 应用入口
对于更大型的项目,可以考虑按功能模块组织:
code复制src/
├── modules/
│ ├── auth/ # 认证模块
│ │ ├── api.js
│ │ ├── components/
│ │ └── store.js
│ └── user/ # 用户模块
│ ├── api.js
│ ├── components/
│ └── store.js
├── core/ # 核心功能
│ ├── api/
│ ├── router/
│ └── stores/
└── shared/ # 共享资源
├── components/
└── utils/
这种组织方式使得每个功能模块都是自包含的,便于维护和复用。