Vue 全家桶深度探索:从语法精要到项目实战
在现代前端开发中,Vue.js 以其渐进式框架的特性和友好的学习曲线,赢得了大量开发者的青睐。今天,就让我们一起来深入探索 Vue 全家桶的魅力所在!
一、Vue 与 React:理念的碰撞
在开始 Vue 全家桶的深度探索之前,让我们先来理解 Vue 与 React 这对"欢喜冤家"的核心差异。
1.1 设计哲学对比
React 推崇函数式编程思想,强调不可变性和单向数据流。它的核心理念是:
- 单向数据绑定:
数据 -> 视图+事件 -> 数据更新 - 一切都是 JavaScript:JSX 将标记与逻辑耦合
- 手动优化:需要开发者关注性能优化点
Vue 则更倾向于渐进式和响应式:
- 双向数据绑定:
v-model指令简化表单处理 - 关注点分离:模板、逻辑、样式相对独立
- 自动优化:响应式系统自动处理依赖追踪
1.2 代码风格对比
让我们通过一个简单的计数器组件来感受两者的差异:
React 实现:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数: {count}</p>
<button onClick={() => setCount(count + 1)}>
点击+1
</button>
</div>
);
}
Vue 实现:
<template>
<div>
<p>当前计数: {{ count }}</p>
<button @click="count++">
点击+1
</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
可以看到,Vue 的 ref 和模板语法让代码更加简洁直观。特别是 @click="count++" 这种直接修改的方式,体现了 Vue 响应式系统的便利性。
二、Vue 3 语法精要
2.1 SFC:单文件组件的艺术
Vue 的单文件组件(Single File Component)是我最喜欢的设计之一。它将一个组件的模板、逻辑和样式封装在单个 .vue 文件中:
<template>
<!-- 视图层 -->
<div class="greeting">{{ message }}</div>
</template>
<script setup>
// 逻辑层
import { ref } from 'vue';
const message = ref('Hello Vue!');
</script>
<style scoped>
/* 样式层 */
.greeting {
color: #42b983;
font-size: 1.5rem;
}
</style>
这种组织方式让组件变得高度可维护,特别是 <style scoped> 中的样式作用域机制,完美解决了 CSS 污染问题。
2.2 模板语法:声明式渲染的魅力
Vue 的模板语法既强大又直观:
<template>
<div>
<!-- 文本插值 -->
<h1>{{ title }}</h1>
<!-- 属性绑定 -->
<img :src="avatarUrl" :alt="userName">
<!-- 条件渲染 -->
<div v-if="isVisible">你看得见我!</div>
<div v-else>现在看不见了</div>
<!-- 列表渲染 -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<!-- 双向绑定 -->
<input v-model="searchText" placeholder="搜索...">
</div>
</template>
2.3 响应式系统:Vue 的灵魂
Vue 3 的响应式系统基于 Proxy,提供了 ref 和 reactive 两种 API:
<script setup>
import { ref, reactive, computed, watch } from 'vue';
// 基本类型使用 ref
const count = ref(0);
const searchField = ref('');
// 对象类型可以使用 reactive
const user = reactive({
name: '张三',
age: 25,
profile: {
level: 'VIP'
}
});
// 计算属性
const userInfo = computed(() => {
return `${user.name} - ${user.age}岁`;
});
// 侦听器
watch(count, (newValue, oldValue) => {
console.log(`计数从 ${oldValue} 变为 ${newValue}`);
});
// 方法
const increment = () => {
count.value++;
};
</script>
响应式的重要提示:
-
ref创建的值在 JS 中访问需要使用.value -
reactive创建的对象可以直接访问属性 - 模板中都不需要
.value,Vue 会自动解包
三、Pinia:新一代状态管理
Pinia 是 Vue 官方推荐的状态管理库,相比 Vuex,它更加简洁和类型安全。
3.1 Store 的定义与使用
// store/homeStore.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';
import type { HomeTopBarItem, RecentlyViewedItem } from '@/types/home';
export const useHomeStore = defineStore('home', () => {
// 状态定义
const topBarState = ref<HomeTopBarItem[]>([
{
title: "游览&体验",
icon: 'photo-o'
},
// ... 更多项
]);
const recentlyViewedState = ref<RecentlyViewedItem[]>([
{
title: "曼谷 & 芭达雅景点通票",
cover: "https://example.com/image.jpg",
price: 173,
},
// ... 更多项
]);
// Getter(计算属性)
const expensiveItems = computed(() => {
return recentlyViewedState.value.filter(item => item.price > 100);
});
// Action(方法)
const addRecentlyViewed = (item: RecentlyViewedItem) => {
recentlyViewedState.value.unshift(item);
// 保持最近浏览不超过10个
if (recentlyViewedState.value.length > 10) {
recentlyViewedState.value.pop();
}
};
return {
topBarState,
recentlyViewedState,
expensiveItems,
addRecentlyViewed
};
});
3.2 在组件中使用 Store
<template>
<div class="home">
<van-search
v-model="searchField"
placeholder="请输入搜索关键词"
show-action
shape="round"
>
<template #action>
<div class="text-white">
<van-icon name="shopping-cart-o" size="1.25rem" />
</div>
</template>
</van-search>
<section class="topbar flex justify-around mb-3">
<div
v-for="item in topBarState"
:key="item.title"
class="topbar-item"
>
<van-icon :name="item.icon" size="2rem" />
<span class="text-xs">{{ item.title }}</span>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { toRefs, ref, onMounted } from 'vue';
import { useHomeStore } from '@/store/homeStore';
const searchField = ref<string>('');
const homeStore = useHomeStore();
// 使用 toRefs 保持响应式
const { topBarState, recentlyViewedState } = toRefs(homeStore);
// 直接使用 action
const handleAddItem = () => {
homeStore.addRecentlyViewed({
title: "新景点",
cover: "https://example.com/new.jpg",
price: 200,
});
};
onMounted(() => {
// 组件挂载后可以执行初始化操作
console.log('Home 组件已挂载');
});
</script>
为什么使用 toRefs?
- 当从 store 中解构状态时,使用
toRefs可以保持响应式 - 否则直接解构会失去响应式连接
四、路由管理:Vue Router 深度应用
4.1 路由配置与类型安全
// router/index.ts
import {
createRouter,
createWebHistory,
type RouteRecordRaw
} from 'vue-router';
// 使用 TypeScript 确保路由配置正确
const rootRoutes: RouteRecordRaw[] = [
{
path: '/home',
component: () => import('@/views/HomePage/HomePage.vue'),
name: 'home',
meta: {
title: '首页',
requiresAuth: false
}
},
{
path: '/account',
component: () => import('@/views/Account/Account.vue'),
name: 'account',
meta: {
title: '我的账户',
requiresAuth: true
}
},
// ... 更多路由
];
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'App',
component: () => import('@/views/TheRoot.vue'),
redirect: '/home',
children: rootRoutes
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
// 路由守卫
router.beforeEach((to, from) => {
// 修改页面标题
if (to.meta.title) {
document.title = to.meta.title as string;
}
// 身份验证检查
if (to.meta.requiresAuth && !isLoggedIn()) {
return { name: 'login' };
}
});
export default router;
4.2 布局组件与路由视图
<!-- App.vue -->
<template>
<div id="app">
<router-view />
<TabBar v-if="showTabBar" />
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import TabBar from '@/views/layout/TabBar.vue';
const route = useRoute();
// 根据当前路由决定是否显示底部导航
const showTabBar = computed(() => {
const hiddenRoutes = ['/login', '/register'];
return !hiddenRoutes.includes(route.path);
});
</script>
<!-- TheRoot.vue -->
<template>
<div class="root-layout">
<header v-if="showHeader" class="app-header">
<van-nav-bar
:title="currentTitle"
left-arrow
@click-left="router.back()"
/>
</header>
<main class="app-main">
<router-view />
</main>
<footer class="app-footer">
<TabBar />
</footer>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
const currentTitle = computed(() => route.meta.title || '默认标题');
const showHeader = computed(() => !route.meta.hideHeader);
</script>
五、插槽系统:组件的灵活性之源
Vue 的插槽系统让组件具备了极高的可定制性。
5.1 基础插槽使用
<!-- BaseCard.vue -->
<template>
<div class="card">
<div class="card-header">
<!-- 具名插槽 -->
<slot name="header">
<!-- 默认内容 -->
<h3>默认标题</h3>
</slot>
</div>
<div class="card-body">
<!-- 默认插槽 -->
<slot>
<p>默认内容</p>
</slot>
</div>
<div class="card-actions">
<!-- 作用域插槽 -->
<slot name="actions" :item="itemData" :isFavorite="isFavorite">
<button @click="handleDefaultAction">默认操作</button>
</slot>
</div>
</div>
</template>
5.2 插槽的使用
<template>
<BaseCard>
<!-- 具名插槽使用 -->
<template #header>
<div class="custom-header">
<van-icon name="star" />
<h3>自定义标题</h3>
</div>
</template>
<!-- 默认插槽内容 -->
<p>这是卡片的主要内容...</p>
<!-- 作用域插槽使用 -->
<template #actions="{ item, isFavorite }">
<van-button
:type="isFavorite ? 'primary' : 'default'"
@click="toggleFavorite(item)"
>
{{ isFavorite ? '已收藏' : '收藏' }}
</van-button>
<van-button @click="viewDetail(item)">
查看详情
</van-button>
</template>
</BaseCard>
</template>
六、TypeScript:Vue 的完美搭档
TypeScript 为 Vue 开发带来了类型安全和更好的开发体验。
6.1 类型定义
// types/home.ts
export interface HomeTopBarItem {
title: string;
icon: string;
badge?: number; // 可选属性
}
export interface RecentlyViewedItem {
id: string;
title: string;
cover: string;
price: number;
originalPrice?: number;
rating?: number;
}
// 泛型响应类型
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
timestamp: number;
}
// 组件 Props 类型
export interface RecentlyViewedProps {
items: RecentlyViewedItem[];
maxDisplay?: number;
showPrice?: boolean;
}
6.2 组合式函数与类型
// composables/useApi.ts
import { ref, type Ref } from 'vue';
import { request } from '@/utils/request';
export function useApi<T>(url: string) {
const data: Ref<T | null> = ref(null);
const loading = ref(false);
const error = ref<string | null>(null);
const fetchData = async (params?: Record<string, any>) => {
loading.value = true;
error.value = null;
try {
const response = await request<T>({
url,
method: 'GET',
params
});
data.value = response.data;
} catch (err) {
error.value = err instanceof Error ? err.message : '未知错误';
} finally {
loading.value = false;
}
};
return {
data,
loading,
error,
fetchData
};
}
七、工具链与工程化
7.1 Vite 配置优化
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import { VantResolver } from '@vant/auto-import-resolver';
import path from 'path';
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [VantResolver()],
dts: true, // 生成类型声明文件
}),
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
7.2 请求封装与拦截器
// utils/request.ts
import axios from 'axios';
import type {
AxiosRequestConfig,
AxiosResponse,
AxiosError
} from 'axios';
import { showToast } from 'vant';
const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10 * 1000,
withCredentials: true,
});
// 请求拦截器
instance.interceptors.request.use(
(config) => {
// 添加认证 token
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
instance.interceptors.response.use(
(response) => {
return response;
},
(error: AxiosError) => {
const status = error.response?.status;
switch (status) {
case 401:
showToast('请先登录');
// 跳转到登录页
break;
case 403:
showToast('没有权限');
break;
case 500:
showToast('服务器错误');
break;
default:
showToast('网络错误');
}
return Promise.reject(error);
}
);
// 泛型请求函数
export const request = <T = any>(
config: AxiosRequestConfig
): Promise<AxiosResponse<T>> => {
return instance(config);
};
// 具体的 API 请求
export const api = {
get: <T = any>(url: string, params?: any) =>
request<T>({ method: 'GET', url, params }),
post: <T = any>(url: string, data?: any) =>
request<T>({ method: 'POST', url, data }),
put: <T = any>(url: string, data?: any) =>
request<T>({ method: 'PUT', url, data }),
delete: <T = any>(url: string) =>
request<T>({ method: 'DELETE', url }),
};
八、样式与 Tailwind CSS
8.1 原子化 CSS 的优势
<template>
<div class="home">
<!-- 渐变背景 -->
<div class="top-bg absolute h-36 -z-10 w-screen
bg-gradient-to-b from-orange-500 to-white">
</div>
<!-- 搜索框 -->
<van-search
class="mb-2 mx-4 rounded-lg shadow-sm"
background="transparent"
/>
<!-- 内容区域 -->
<main class="flex flex-col space-y-4 px-4">
<header class="w-[calc(100vw-2rem)] min-h-24
bg-white rounded-2xl p-4 shadow-md
self-center transition-all
hover:shadow-lg">
<!-- 响应式设计 -->
<section class="topbar flex justify-around
flex-wrap gap-4 mb-4
md:flex-nowrap md:gap-0">
<div
v-for="item in topBarState"
:key="item.title"
class="topbar-item flex flex-col items-center
cursor-pointer transition-transform
hover:scale-105 min-w-[60px]"
>
<div class="topbar-item__icon mb-1">
<van-icon :name="item.icon"
class="text-2xl text-orange-500" />
</div>
<div class="topbar-item__text text-xs
text-gray-600 font-medium">
{{ item.title }}
</div>
</div>
</section>
</header>
</main>
</div>
</template>
8.2 自定义样式与 Tailwind 结合
/* style.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 自定义组件样式 */
@layer components {
.btn-primary {
@apply bg-orange-500 text-white px-4 py-2
rounded-lg hover:bg-orange-600
transition-colors focus:outline-none
focus:ring-2 focus:ring-orange-300
disabled:opacity-50 disabled:cursor-not-allowed;
}
.card {
@apply bg-white rounded-xl shadow-sm
border border-gray-100
hover:shadow-md transition-shadow;
}
}
/* 自定义工具类 */
@layer utilities {
.text-shadow {
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}
九、项目架构最佳实践
9.1 目录结构设计
src/
├── assets/ # 静态资源
│ ├── images/
│ └── styles/
├── components/ # 通用组件
│ ├── ui/ # 基础UI组件
│ └── business/ # 业务组件
├── views/ # 页面组件
├── store/ # 状态管理
│ ├── modules/ # 模块化store
│ └── index.ts
├── router/ # 路由配置
├── utils/ # 工具函数
│ ├── request.ts
│ └── helpers.ts
├── types/ # 类型定义
├── composables/ # 组合式函数
├── api/ # API接口
└── main.ts
9.2 组件设计原则
<!-- 好的组件设计示例 -->
<template>
<ProductCard
:product="product"
:show-price="true"
:favorite="isFavorite"
@favorite-toggle="handleFavoriteToggle"
@click="handleCardClick"
>
<template #badge>
<van-tag v-if="product.isNew" type="primary">
新品
</van-tag>
</template>
<template #actions>
<van-button
size="small"
type="primary"
@click.stop="handleBuy"
>
立即购买
</van-button>
</template>
</ProductCard>
</template>
<script setup lang="ts">
// 明确的 Props 定义
interface Props {
product: Product;
showPrice?: boolean;
favorite?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
showPrice: true,
favorite: false
});
// 明确的事件定义
const emit = defineEmits<{
'favorite-toggle': [id: string, value: boolean];
click: [product: Product];
}>();
// 组合式函数复用
const { toggleFavorite } = useFavorite();
const { addToCart } = useCart();
const handleFavoriteToggle = async () => {
const newValue = !props.favorite;
await toggleFavorite(props.product.id, newValue);
emit('favorite-toggle', props.product.id, newValue);
};
const handleCardClick = () => {
emit('click', props.product);
};
</script>
十、性能优化与最佳实践
10.1 组件性能优化
<template>
<!-- 虚拟滚动优化长列表 -->
<RecycleScroller
:items="largeList"
:item-size="80"
key-field="id"
v-slot="{ item }"
class="h-96"
>
<ProductItem :item="item" />
</RecycleScroller>
<!-- 图片懒加载 -->
<img
v-for="image in images"
:key="image.id"
v-lazy="image.url"
class="product-image"
alt="产品图片"
/>
</template>
<script setup>
import { computed, watchEffect, shallowRef } from 'vue';
// 使用 shallowRef 避免深度响应式
const largeList = shallowRef([]);
// 计算属性缓存
const filteredList = computed(() => {
return largeList.value.filter(item =>
item.price > 0 && item.stock > 0
);
});
// 监听优化
watchEffect(() => {
// 只有当依赖变化时才执行
if (filteredList.value.length > 0) {
updateStatistics(filteredList.value);
}
});
// 函数记忆化
const expensiveCalculation = computed(() => {
return heavyCalculation(filteredList.value);
});
</script>
10.2 打包优化
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-library': ['vant'],
'utils': ['axios', 'dayjs', 'lodash-es']
}
}
},
chunkSizeWarningLimit: 1000
}
});
总结
Vue 全家桶提供了一个完整、优雅的前端解决方案。从响应式系统到状态管理,从路由控制到构建工具,每一个环节都体现了 Vue 团队对开发者体验的深度思考。
Vue 的核心优势:
- 渐进式:可以根据项目需求逐步采用
- 响应式:自动的依赖追踪和更新
- 组合式:优秀的逻辑复用能力
- 类型友好:与 TypeScript 完美结合
- 生态丰富:完整的工具链和组件库
无论是初创项目还是大型企业应用,Vue 全家桶都能提供出色的开发体验和运行性能。希望这篇笔记能帮助你更好地理解和运用 Vue 全家桶,在开发路上越走越远!