一、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,提供了 refreactive 两种 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 的核心优势:

  1. 渐进式:可以根据项目需求逐步采用
  2. 响应式:自动的依赖追踪和更新
  3. 组合式:优秀的逻辑复用能力
  4. 类型友好:与 TypeScript 完美结合
  5. 生态丰富:完整的工具链和组件库

无论是初创项目还是大型企业应用,Vue 全家桶都能提供出色的开发体验和运行性能。希望这篇笔记能帮助你更好地理解和运用 Vue 全家桶,在开发路上越走越远!

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]