摘要:Vue 3作为前端主流框架,相比Vue 2,引入了Composition API、Teleport、Suspense等核心特性,大幅提升了代码的可维护性与扩展性;Vite作为新一代构建工具,凭借其基于ES Module的即时热更新、快速构建速度,替代Webpack成为Vue 3项目的首选构建工具;Pinia作为Vue官方推荐的状态管理库,替代Vuex,简化了状态管理逻辑,支持TypeScript、模块化开发。本文基于Vue 3.4、Vite 5.0、Pinia 2.1,详细讲解前端工程化配置、Composition API实战、组件化开发、状态管理、路由配置、打包优化等核心知识点,结合实战场景(后台管理系统),附完整项目结构与代码案例,帮助前端开发者快速掌握Vue 3+Vite+Pinia的实战技巧,突破前端工程化与组件化瓶颈,适合Vue入门到进阶的学习者、前端开发工程师。
一、前言:Vue 3+Vite+Pinia的核心价值与技术优势
随着前端技术的快速发展,工程化、组件化、类型安全成为前端开发的核心需求。Vue 3相比Vue 2,核心升级在于Composition API,解决了Vue 2 Options API在大型项目中代码复用困难、逻辑分散的问题;Vite相比Webpack,采用"按需编译"模式,避免了Webpack的全量打包,热更新速度提升10倍以上,构建效率大幅提升;Pinia相比Vuex,简化了状态管理流程,取消了Mutations、Modules的繁琐配置,支持TypeScript原生集成,更适合现代前端工程化开发。
三者结合,形成了"Vue 3(核心框架)+ Vite(构建工具)+ Pinia(状态管理)"的主流前端技术栈,广泛应用于后台管理系统、移动端应用、PC端官网等场景,能够大幅提升开发效率、代码质量与项目可维护性。本文聚焦实战,从项目初始化到工程化配置,再到组件化开发与状态管理,逐步讲解整套技术栈的落地技巧。
二、核心基础:项目初始化与工程化配置
2.1 项目初始化(Vue 3+Vite+Pinia+TypeScript)
bash
# 1. 初始化Vite+Vue 3项目(TypeScript)
npm create vite@latest vue3-admin --template vue-ts
# 2. 进入项目目录
cd vue3-admin
# 3. 安装核心依赖
npm install pinia vue-router@4 axios element-plus @element-plus/icons-vue
# 安装开发依赖
npm install -D sass eslint prettier eslint-plugin-vue @types/node
# 4. 启动开发服务器(热更新)
npm run dev
# 5. 构建生产环境
npm run build
# 6. 预览生产环境打包结果
npm run preview
2.2 项目目录结构设计(工程化规范)
text
vue3-admin/
├── public/ # 静态资源(图片、字体,无需打包)
├── src/
│ ├── assets/ # 静态资源(样式、图片,需打包)
│ │ ├── css/ # 全局样式
│ │ └── images/ # 图片资源
│ ├── components/ # 组件目录
│ │ ├── common/ # 通用组件(按钮、表单、分页、弹窗)
│ │ ├── layout/ # 布局组件(侧边栏、顶部导航、页脚)
│ │ └── business/ # 业务组件(对应具体业务模块)
│ ├── router/ # 路由配置(Vue Router 4)
│ │ └── index.ts # 路由入口
│ ├── store/ # 状态管理(Pinia)
│ │ ├── modules/ # 模块化状态(用户、权限、设置)
│ │ └── index.ts # Pinia初始化
│ ├── views/ # 页面组件(对应路由页面)
│ │ ├── Login/ # 登录页
│ │ ├── Home/ # 首页
│ │ ├── User/ # 用户管理页
│ │ └── Role/ # 角色管理页
│ ├── api/ # 接口请求封装(axios)
│ │ ├── request.ts # axios配置(拦截器、基础路径)
│ │ ├── user.ts # 用户相关接口
│ │ └── role.ts # 角色相关接口
│ ├── utils/ # 工具函数
│ │ ├── auth.ts # 权限工具(token存储、权限判断)
│ │ └── common.ts # 通用工具(格式转换、防抖节流)
│ ├── types/ # TypeScript类型定义
│ ├── App.vue # 根组件
│ ├── main.ts # 入口文件(初始化Vue、Pinia、Router)
│ └── env.d.ts # 环境变量类型定义
├── .eslintrc.js # ESLint配置(代码规范)
├── .prettierrc.js # Prettier配置(代码格式化)
├── vite.config.ts # Vite配置(构建、开发服务器、别名)
├── tsconfig.json # TypeScript配置
└── package.json # 依赖配置
2.3 核心工程化配置(Vite+ESLint+Prettier)
工程化配置是保证项目规范、提升开发效率的关键,以下配置Vite别名、ESLint代码规范、Prettier代码格式化,确保团队开发规范统一。
typescript
// 1. Vite配置(vite.config.ts)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
// 配置别名(简化路径引入)
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@assets': path.resolve(__dirname, './src/assets'),
'@components': path.resolve(__dirname, './src/components'),
'@views': path.resolve(__dirname, './src/views')
}
},
// 开发服务器配置
server: {
port: 3000, // 端口号
open: true, // 自动打开浏览器
proxy: {
// 接口代理(解决跨域)
'/api': {
target: 'http://localhost:8080', // 后端接口地址
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
// 构建配置
build: {
outDir: 'dist', // 打包输出目录
sourcemap: false, // 关闭sourcemap(生产环境)
// 打包优化:拆分代码块
rollupOptions: {
output: {
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]'
}
}
}
})
// 2. ESLint配置(.eslintrc.js)
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:vue/vue3-essential',
'plugin:@typescript-eslint/recommended',
'prettier' // 与Prettier集成,避免冲突
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser',
sourceType: 'module'
},
plugins: [
'vue',
'@typescript-eslint'
],
rules: {
// 自定义规则
'vue/multi-word-component-names': 'off', // 关闭组件名多单词限制
'@typescript-eslint/no-unused-vars': 'warn', // 未使用变量警告
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off' // 生产环境禁止console
}
}
// 3. Prettier配置(.prettierrc.js)
module.exports = {
printWidth: 120, // 每行最大长度
tabWidth: 2, // 缩进宽度
useTabs: false, // 使用空格缩进
singleQuote: true, // 单引号
semi: true, // 句末加分号
trailingComma: 'es5', // 数组、对象末尾加逗号
bracketSpacing: true, // 括号前后加空格
arrowParens: 'always', // 箭头函数参数必须加括号
proseWrap: 'never', // 不换行
htmlWhitespaceSensitivity: 'ignore' // 忽略HTML空格敏感
}
三、实战模块:核心功能实战与落地
3.1 模块1:路由配置(Vue Router 4)
Vue Router 4是Vue 3的官方路由工具,支持路由懒加载、嵌套路由、路由守卫等功能,适合构建复杂的后台管理系统路由。
typescript
// 1. 路由配置(src/router/index.ts)
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { useUserStore } from '@/store/modules/user'
import Layout from '@/components/layout/Layout.vue'
// 路由规则
const routes: RouteRecordRaw[] = [
// 登录页(无需布局)
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login/Login.vue'),
meta: {
title: '登录',
requireAuth: false // 无需登录权限
}
},
// 主布局路由(嵌套路由)
{
path: '/',
component: Layout,
meta: {
requireAuth: true // 需要登录权限
},
children: [
// 首页
{
path: '',
name: 'Home',
component: () => import('@/views/Home/Home.vue'),
meta: {
title: '首页'
}
},
// 用户管理
{
path: '/user',
name: 'User',
component: () => import('@/views/User/User.vue'),
meta: {
title: '用户管理',
permission: 'user:view' // 所需权限
}
},
// 角色管理
{
path: '/role',
name: 'Role',
component: () => import('@/views/Role/Role.vue'),
meta: {
title: '角色管理',
permission: 'role:view'
}
}
]
},
// 404页面
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound/NotFound.vue'),
meta: {
title: '页面不存在',
requireAuth: false
}
}
]
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 路由守卫(全局前置守卫,判断登录与权限)
router.beforeEach((to, from, next) => {
// 设置页面标题
document.title = to.meta.title as string || 'Vue3后台管理系统'
const userStore = useUserStore()
// 判断是否需要登录
if (to.meta.requireAuth) {
// 未登录,跳转到登录页
if (!userStore.token) {
next({ name: 'Login', query: { redirect: to.fullPath } })
} else {
// 判断是否有权限
const permission = to.meta.permission as string
if (!permission || userStore.permissions.includes(permission)) {
next()
} else {
// 无权限,跳转到首页
next({ name: 'Home' })
}
}
} else {
next()
}
})
export default router
3.2 模块2:状态管理(Pinia)
Pinia简化了状态管理逻辑,无需Mutations,直接通过Actions修改状态,支持模块化开发与TypeScript类型提示,以下实现用户状态管理(登录、退出、权限控制)。
typescript
// 1. Pinia初始化(src/store/index.ts)
import { createPinia } from 'pinia'
// 创建Pinia实例
const pinia = createPinia()
export default pinia
// 2. 用户状态模块(src/store/modules/user.ts)
import { defineStore } from 'pinia'
import { login, getUserInfo, logout } from '@/api/user'
import { setToken, getToken, removeToken } from '@/utils/auth'
// 定义用户状态类型
interface UserState {
token: string | null
username: string
avatar: string
permissions: string[] // 权限列表
}
// 创建用户Store
export const useUserStore = defineStore('user', {
state: (): UserState => ({
token: getToken(), // 从本地存储获取token
username: '',
avatar: '',
permissions: []
}),
actions: {
// 登录
async loginAction(username: string, password: string) {
const res = await login({ username, password })
// 存储token
this.token = res.token
setToken(res.token)
// 获取用户信息
await this.getUserInfoAction()
},
// 获取用户信息
async getUserInfoAction() {
const res = await getUserInfo()
this.username = res.username
this.avatar = res.avatar
this.permissions = res.permissions
},
// 退出登录
async logoutAction() {
await logout()
// 清空状态
this.token = null
this.username = ''
this.avatar = ''
this.permissions = []
// 移除本地token
removeToken()
}
}
})
// 3. 权限工具(src/utils/auth.ts)
// 存储token到localStorage
export const setToken = (token: string) => {
localStorage.setItem('token', token)
}
// 从localStorage获取token
export const getToken = () => {
return localStorage.getItem('token')
}
// 移除token
export const removeToken = () => {
localStorage.removeItem('token')
}
// 判断是否有权限
export const hasPermission = (permissions: string[], permission: string) => {
return permissions.includes('admin') || permissions.includes(permission)
}
3.3 模块3:Composition API实战(组件化开发)
Composition API是Vue 3的核心特性,通过setup函数(或<script setup>语法糖)将逻辑聚合,实现代码复用,以下实现用户列表组件,结合Pinia、axios、Element Plus,实现查询、分页、删除等功能。
vue
// 1. 用户列表组件(src/views/User/User.vue)
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ElTable, ElTableColumn, ElPagination, ElButton, ElDialog, ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus'
import { Delete, Edit, Search } from '@element-plus/icons-vue'
import { useUserStore } from '@/store/modules/user'
import { getUsers, deleteUser } from '@/api/user'
// 分页参数
const pageNum = ref(1)
const pageSize = ref(10)
const total = ref(0)
// 查询条件
const searchForm = ref({
username: ''
})
// 用户列表数据
const userList = ref([])
// 加载状态
const loading = ref(false)
const userStore = useUserStore()
// 获取用户列表
const getUserList = async () => {
loading.value = true
try {
const res = await getUsers({
pageNum: pageNum.value,
pageSize: pageSize.value,
username: searchForm.value.username
})
userList.value = res.list
total.value = res.total
} catch (error) {
ElMessage.error('获取用户列表失败,请稍后再试')
} finally {
loading.value = false
}
}
// 分页切换
const handlePageChange = (num: number) => {
pageNum.value = num
getUserList()
}
// 每页条数切换
const handlePageSizeChange = (size: number) => {
pageSize.value = size
pageNum.value = 1
getUserList()
}
// 搜索
const handleSearch = () => {
pageNum.value = 1
getUserList()
}
// 删除用户
const handleDelete = async (id: number) => {
try {
await deleteUser(id)
ElMessage.success('删除用户成功')
getUserList() // 重新获取列表
} catch (error) {
ElMessage.error('删除用户失败,请稍后再试')
}
}
// 组件挂载时获取用户列表
onMounted(() => {
getUserList()
})
// 计算属性:判断是否有删除权限
const hasDeletePermission = computed(() => {
return userStore.permissions.includes('user:delete')
})
</script>
<template>
<div class="user-container">
<div class="search-bar">
<ElForm :model="searchForm" inline>
<ElFormItem label="用户名">
<ElInput v-model="searchForm.username" placeholder="请输入用户名" />
</ElFormItem>
<ElFormItem>
<ElButton type="primary" icon="<Search />" @click="handleSearch">搜索</ElButton>
</ElFormItem>
</ElForm>
</div>
<ElTable :data="userList" border loading="loading" style="width: 100%; margin-top: 16px;">
<ElTableColumn label="ID" prop="id" align="center" />
<ElTableColumn label="用户名" prop="username" align="center" />
<ElTableColumn label="手机号" prop="phone" align="center" />
<ElTableColumn label="角色" prop="roleName" align="center" />
<ElTableColumn label="创建时间" prop="createTime" align="center" />
<ElTableColumn label="操作" align="center">
<template #default="scope">
<ElButton type="text" icon="<Edit />">编辑</ElButton>
<ElButton
type="text"
icon="<Delete />"
text-color="red"
@click="handleDelete(scope.row.id)"
:disabled="!hasDeletePermission"
>删除</ElButton>
</template>
</ElTableColumn>
</ElTable>
<div class="pagination" style="margin-top: 16px; text-align: right;">
<ElPagination
v-model:current-page="pageNum"
v-model:page-size="pageSize"
:total="total"
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
layout="total, sizes, prev, pager, next, jumper"
/>
</div>
</div>
</template>
<style scoped lang="scss">
.user-container {
padding: 20px;
.search-bar {
display: flex;
justify-content: space-between;
align-items: center;
}
.pagination {
margin-top: 16px;
}
}
</style>
3.4 模块4:接口请求封装(axios)
封装axios,实现请求拦截器(添加token)、响应拦截器(统一错误处理),简化接口调用,提升代码复用性。
typescript
// 1. axios配置(src/api/request.ts)
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getToken, removeToken } from '@/utils/auth'
import { useUserStore } from '@/store/modules/user'
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量(不同环境不同地址)
timeout: 5000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
// 请求拦截器(添加token)
service.interceptors.request.use(
(config) => {
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器(统一错误处理)
service.interceptors.response.use(
(response) => {
const res = response.data
// 状态码非200,视为错误
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败')
// 401:未登录或token过期,跳转登录页
if (res.code === 401) {
const userStore = useUserStore()
ElMessageBox.confirm('登录已过期,请重新登录', '提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
userStore.logoutAction()
window.location.href = '/login'
})
}
return Promise.reject(res)
}
return res
},
(error) => {
// 网络错误处理
ElMessage.error('网络异常,请检查网络连接')
return Promise.reject(error)
}
)
export default service
// 2. 用户接口封装(src/api/user.ts)
import request from './request'
// 登录
export const login = (data: { username: string; password: string }) => {
return request({
url: '/user/login',
method: 'POST',
data
})
}
// 获取用户信息
export const getUserInfo = () => {
return request({
url: '/user/info',
method: 'GET'
})
}
// 退出登录
export const logout = () => {
return request({
url: '/user/logout',
method: 'POST'
})
}
// 获取用户列表
export const getUsers = (params: { pageNum: number; pageSize: number; username?: string }) => {
return request({
url: '/user/list',
method: 'GET',
params
})
}
// 删除用户
export const deleteUser = (id: number) => {
return request({
url: `/user/${id}`,
method: 'DELETE'
})
}
四、打包优化与生产环境部署
4.1 打包优化技巧
Vite默认已做了基础优化,结合以下技巧,进一步提升生产环境打包性能,减少包体积,提升加载速度。
typescript
// 1. 按需引入Element Plus(减少包体积)
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './store'
// 按需引入Element Plus组件与样式
import { ElButton, ElTable, ElInput, ElPagination, ElDialog, ElForm, ElFormItem, ElMessage, ElMessageBox } from 'element-plus'
import 'element-plus/dist/index.css'
// 引入图标
import { Search, Delete, Edit } from '@element-plus/icons-vue'
const app = createApp(App)
// 注册组件
app.component('ElButton', ElButton)
app.component('ElTable', ElTable)
app.component('ElInput', ElInput)
app.component('ElPagination', ElPagination)
app.component('ElDialog', ElDialog)
app.component('ElForm', ElForm)
app.component('ElFormItem', ElFormItem)
// 注册图标
app.component('Search', Search)
app.component('Delete', Delete)
app.component('Edit', Edit)
// 全局挂载ElMessage、ElMessageBox
app.config.globalProperties.$message = ElMessage
app.config.globalProperties.$confirm = ElMessageBox.confirm
app.use(router).use(pinia).mount('#app')