Vue前端开发实战:从入门到工程化

前言(扩展)

Vue.js自2014年发布以来,凭借"渐进式框架"的设计哲学,从一个轻量级视图库成长为覆盖全场景的前端解决方案。无论是个人开发者快速搭建原型,还是企业级团队开发复杂系统,Vue都能提供灵活的技术路径。本书的核心目标是**"让读者从'会用Vue'到'精通Vue工程化'"**,因此内容设计上遵循"基础→进阶→实战→扩展"的递进逻辑,每章均包含:

  • 核心概念解析(结合原理图解,避免死记硬背)
  • 完整代码示例(可直接复制运行的实战代码)
  • 避坑指南(总结开发中常见的10+类问题及解决方案)
  • 工程实践(从单文件组件到企业级架构的设计思路)

无论你是希望提升技能的前端开发者,还是需要构建完整项目的团队技术负责人,本书都能为你提供可落地的经验参考。

第一部分 基础篇:Vue核心能力构建

第1章 Vue 3快速上手:从0到1搭建第一个应用(详细)

1.1 Vue 3新特性深度解析(对比Vue 2)

Vue 3的升级不是简单的"修修补补",而是对核心架构的重构,以下是需要重点掌握的四大变革:

特性 Vue 2 Vue 3 核心价值
响应式系统 Object.defineProperty Proxy 支持嵌套对象/数组的深层响应、更少的性能损耗
组合式API 选项式API(data/methods setup/<script setup> 逻辑复用更灵活、TypeScript友好
编译优化 全量编译 静态标记(PatchFlag) 运行时性能提升(渲染速度+30%+)
TypeScript支持 需手动配置 原生支持(defineComponent 大型项目类型安全更有保障

示例:Proxy响应式原理简析

Vue 3通过Proxy拦截对象的所有操作(get/set/deleteProperty),相比Object.defineProperty只能监听已有属性,Proxy可以动态追踪新增/删除的属性,因此无需手动调用Vue.setVue.delete

javascript 复制代码
// Vue 3响应式核心代码简化版(源码参考:@vue/reactivity)
function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      track(target, key); // 收集依赖(用于触发更新)
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      const result = Reflect.set(target, key, value);
      trigger(target, key); // 触发依赖(更新视图)
      return result;
    }
  });
}

1.2 开发环境搭建(保姆级步骤)

前置条件:需安装Node.js 16+(推荐v18),建议使用pnpm作为包管理器(更快、更节省磁盘空间)。

步骤1:创建Vue项目

使用Vue官方推荐的create-vue工具(基于Vite):

bash 复制代码
npm create vue@latest my-vue-app

根据提示选择配置(推荐勾选:TypeScript、Pinia、Vue Router、ESLint),最终生成的项目结构如下:

csharp 复制代码
my-vue-app/
├── public/               # 静态资源(无需打包)
├── src/
│   ├── assets/           # 需打包的静态资源(图片、字体)
│   ├── components/       # 公共组件
│   ├── router/           # 路由配置
│   ├── stores/           # Pinia状态管理
│   ├── views/            # 页面组件
│   ├── App.vue           # 根组件
│   └── main.ts           # 入口文件
├── vite.config.ts        # Vite配置
├── tsconfig.json         # TypeScript配置
└── package.json          # 依赖管理

步骤2:启动项目

bash 复制代码
cd my-vue-app
pnpm install  # 安装依赖
pnpm dev      # 启动开发服务器(默认访问http://localhost:5173)

1.3 第一个Vue应用:待办事项列表(含完整代码)

本实践将实现一个支持添加、删除、状态切换的待办事项列表,覆盖Vue 3核心语法。

步骤1:创建组件TodoList.vue

vue 复制代码
<template>
  <div class="todo-container">
    <h1>待办事项</h1>
    <!-- 输入框+添加按钮 -->
    <div class="input-group">
      <input 
        v-model="newTodo" 
        @keyup.enter="addTodo" 
        placeholder="输入待办事项..."
      />
      <button @click="addTodo">添加</button>
    </div>
    <!-- 待办列表 -->
    <ul class="todo-list">
      <li v-for="(todo, index) in todos" :key="index" :class="{ completed: todo.completed }">
        <input type="checkbox" v-model="todo.completed" />
        <span>{{ todo.text }}</span>
        <button @click="removeTodo(index)">删除</button>
      </li>
    </ul>
    <!-- 统计信息 -->
    <p v-if="todos.length > 0">剩余未完成:{{ remainingCount }} / {{ todos.length }}</p>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

// 响应式数据:待办列表、输入框内容
const todos = ref([]);
const newTodo = ref('');

// 计算属性:剩余未完成数量
const remainingCount = computed(() => {
  return todos.value.filter(todo => !todo.completed).length;
});

// 方法:添加待办
const addTodo = () => {
  if (newTodo.value.trim()) {
    todos.value.push({
      text: newTodo.value.trim(),
      completed: false
    });
    newTodo.value = ''; // 清空输入框
  }
};

// 方法:删除待办
const removeTodo = (index) => {
  todos.value.splice(index, 1);
};
</script>

<style scoped>
.todo-container {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}
.input-group {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}
.todo-list {
  list-style: none;
  padding: 0;
}
.todo-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-bottom: 1px solid #eee;
}
.completed span {
  color: #999;
  text-decoration: line-through;
}
</style>

步骤2:在App.vue中使用组件

vue 复制代码
<template>
  <TodoList />
</template>

<script setup>
import TodoList from './components/TodoList.vue';
</script>

关键知识点总结

  • <script setup>:Vue 3的组合式API语法糖,无需显式导出组件选项,直接编写逻辑。
  • ref:创建基础类型(字符串、数字、布尔值)的响应式变量,访问时需用.value
  • computed:创建计算属性,自动缓存结果(仅依赖变化时重新计算)。
  • v-for:列表渲染,需为每个项提供唯一的:key(推荐使用index仅当列表顺序固定)。

第2章 组合式API:更灵活的逻辑组织方式(详细)

2.1 为什么需要组合式API?(对比选项式API的痛点)

选项式API(Options API)是Vue 2的核心设计,通过datamethodscomputed等选项组织代码。但在中大型项目中,其局限性逐渐显现:

  • 逻辑分散:一个功能可能涉及多个选项(如数据、方法、计算属性),导致代码跳跃阅读。
  • 复用困难 :多个组件共享逻辑需通过mixins,但mixins存在命名冲突、来源不明确等问题。
  • TypeScript支持弱:选项式API的类型推断需要额外配置,大型项目维护成本高。

组合式API(Composition API)通过setup函数或<script setup>语法糖,将逻辑按功能聚合(而非按选项拆分),完美解决了上述问题。

2.2 核心API详解(附实战案例)

2.2.1 setup函数与<script setup>

setup函数是组合式API的入口,执行时机在组件初始化之前(早于datacomputed等选项)。而<script setup>是其语法糖,更简洁且支持顶层await

示例:用setup函数实现计数器

vue 复制代码
<script>
export default {
  setup() {
    const count = ref(0);
    const increment = () => { count.value++; };
    return { count, increment }; // 暴露给模板
  }
};
</script>

<!-- 等价的<script setup>写法 -->
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => { count.value++; };
</script>
2.2.2 生命周期钩子

组合式API提供了更细粒度的生命周期钩子(替代选项式API的mountedupdated等),需从vue中导入。

生命周期钩子 触发时机 替代选项式API
onMounted 组件挂载到DOM后 mounted
onUpdated 组件更新后 updated
onUnmounted 组件卸载前 beforeUnmount
onBeforeMount 组件挂载前 beforeMount

实战:在组件挂载后获取DOM元素

vue 复制代码
<script setup>
import { ref, onMounted } from 'vue';

const inputRef = ref(null);

onMounted(() => {
  // 此时可安全访问DOM
  inputRef.value.focus();
});
</script>

<template>
  <input ref="inputRef" placeholder="自动聚焦" />
</template>
2.2.3 依赖注入(provide/inject

跨层级组件通信时,传统的props逐层传递("prop drilling")会导致代码冗余。provide(提供)和inject(注入)允许祖先组件向任意后代组件传递数据,无需经过中间组件。

实战:主题模式切换(全局状态传递)

vue 复制代码
<!-- 祖先组件:App.vue -->
<script setup>
import { provide, ref } from 'vue';

const isDarkMode = ref(false);
// 提供一个响应式值(key为'theme')
provide('theme', isDarkMode);
</script>

<!-- 深层子组件:Header.vue -->
<script setup>
import { inject } from 'vue';

// 注入祖先提供的'theme'
const isDarkMode = inject('theme');
</script>
2.2.4 自定义Hook:逻辑复用的最佳实践

自定义Hook是组合式API的核心优势之一,它允许将通用逻辑(如表单校验、API请求)封装为可复用的函数,避免代码重复。

实战:封装"表单校验"Hook

需求:复用邮箱、手机号的校验规则,支持实时校验和错误提示。

javascript 复制代码
// hooks/useFormValidation.js
import { ref, computed } from 'vue';

export function useFormValidation(initialValue, rules) {
  const value = ref(initialValue);
  const error = ref('');

  // 校验函数
  const validate = () => {
    let isValid = true;
    for (const rule of rules) {
      if (rule.required && !value.value) {
        error.value = rule.message || '此字段必填';
        isValid = false;
        break;
      }
      if (rule.pattern && !rule.pattern.test(value.value)) {
        error.value = rule.message || '格式不正确';
        isValid = false;
        break;
      }
    }
    return isValid;
  };

  return {
    value,
    error,
    validate,
    // 计算属性:是否校验通过
    is_valid: computed(() => !error.value)
  };
}

使用示例(表单组件)

vue 复制代码
<script setup>
import { useFormValidation } from './hooks/useFormValidation';

// 校验规则:邮箱必填且符合正则
const emailRules = [
  { required: true, message: '请输入邮箱' },
  { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式错误' }
];

// 使用自定义Hook
const email = useFormValidation('', emailRules);

// 提交表单
const submitForm = () => {
  if (email.validate()) {
    console.log('提交成功,邮箱:', email.value);
  }
};
</script>

<template>
  <form @submit.prevent="submitForm">
    <input v-model="email.value" placeholder="输入邮箱" />
    <p v-if="email.error" style="color: red">{{ email.error }}</p>
    <button type="submit">提交</button>
  </form>
</template>

2.3 本章小结

组合式API的核心是**"逻辑聚合"**,通过将相关逻辑(如数据、方法、校验)封装在一起,大幅提升代码的可读性和可维护性。下一章我们将基于这些能力,深入探讨组件化开发的核心------组件通信与设计原则。

第3章 组件化开发:从原子组件到复杂页面(详细)

3.1 组件设计原则(附反例分析)

优秀的组件设计应遵循以下原则,否则可能导致代码冗余、维护困难:

原则 说明 反例
单一职责 一个组件只负责一个功能点 一个"用户信息卡片"组件同时包含头像、昵称、订单列表、编辑按钮
高内聚低耦合 组件内部逻辑自包含,对外暴露清晰接口 组件内部直接操作DOM,依赖外部样式
可复用性 通过Props/Events抽象通用能力 为每个页面单独编写相似的列表组件

反例分析:冗余的"商品列表"组件

某团队早期为不同页面(首页、分类页、购物车页)分别编写了商品列表组件,每个组件的结构相似但筛选逻辑不同。后期维护时,修改一处需同步修改三个组件,耗时耗力。优化方案:抽象出通用ProductList组件,通过filter Prop接收不同的筛选规则,通过onItemClick Event暴露点击事件。

3.2 组件通信全攻略(附场景化解决方案)

3.2.1 父子组件通信(最常用场景)
  • 父→子:Props

    父组件通过props向子组件传递数据,子组件需声明props的类型、默认值等(TypeScript推荐使用defineProps)。

    vue 复制代码
    <!-- 父组件 -->
    <template>
      <ChildComponent :title="pageTitle" :items="listData" />
    </template>
    
    <!-- 子组件 -->
    <script setup>
    // TypeScript方式声明Props(推荐)
    const props = defineProps<{
      title: string;
      items?: string[]; // 可选,默认值为空数组
    }>();
    
    // 简写方式(非TS)
    // const props = defineProps(['title', 'items']);
    </script>
  • 子→父:$emit

    子组件通过defineEmits声明事件,父组件通过@event监听。

    vue 复制代码
    <!-- 子组件 -->
    <script setup>
    const emit = defineEmits<{
      (e: 'select', id: number): void;
    }>();
    
    const handleSelect = (id) => {
      emit('select', id); // 触发事件
    };
    </script>
    
    <!-- 父组件 -->
    <template>
      <ChildComponent @select="onItemSelected" />
    </template>
3.2.2 兄弟组件通信(无直接层级关系)
  • 方案1:通过共同父组件转发 (推荐)

    兄弟A触发事件→父组件接收并更新状态→父组件传递状态给兄弟B。

  • 方案2:事件总线(Event Bus) (慎用,可能导致状态混乱)

    创建一个全局的Vue实例作为事件中心,组件通过$on/$emit通信。

    javascript 复制代码
    // eventBus.js
    import { createApp } from 'vue';
    export const eventBus = createApp({});
    
    // 组件A(发送事件)
    import { eventBus } from './eventBus';
    eventBus.config.globalProperties.$emit('sibling-event', data);
    
    // 组件B(接收事件)
    import { eventBus } from './eventBus';
    eventBus.config.globalProperties.$on('sibling-event', (data) => {
      // 处理数据
    });
3.2.3 跨层级组件通信(祖先→后代)
  • provide/inject (推荐)

    祖先组件通过provide提供数据,后代组件通过inject注入,支持响应式。

    vue 复制代码
    <!-- 祖先组件 -->
    <script setup>
    import { provide, ref } from 'vue';
    
    const theme = ref('light');
    provide('theme', theme); // 提供响应式值
    </script>
    
    <!-- 深层子组件(任意层级) -->
    <script setup>
    import { inject } from 'vue';
    
    const theme = inject('theme'); // 注入数据
    </script>
3.2.4 插槽(Slot):组件的"扩展点"

插槽允许父组件向子组件传递内容(HTML、组件),是实现"通用容器"组件的关键。

  • 默认插槽 :子组件用<slot>声明,父组件直接填充内容。
  • 具名插槽 :子组件用<slot name="header">声明,父组件用v-slot:header指定。
  • 作用域插槽:子组件向父组件传递数据,父组件可自定义渲染逻辑(常用于列表组件)。

实战:通用卡片组件(含作用域插槽)

vue 复制代码
<!-- Card.vue -->
<script setup>
import { defineProps, defineSlots } from 'vue';

const props = defineProps<{
  title: string;
}>();
</script>

<template>
  <div class="card">
    <h3>{{ title }}</h3>
    <!-- 作用域插槽:向父组件传递item数据 -->
    <slot :item="props.item"></slot>
  </div>
</template>
vue 复制代码
<!-- 父组件使用 -->
<Card :title="商品详情" :item="currentProduct">
  <!-- 自定义插槽内容,可访问子组件传递的item -->
  <template #default="{ item }">
    <p>价格:{{ item.price }}</p>
    <p>库存:{{ item.stock }}</p>
  </template>
</Card>

3.3 实战:开发通用弹窗组件(附完整代码)

本实践将开发一个支持自定义内容、动画、遮罩层关闭的弹窗组件,覆盖组件设计的多个核心知识点。

需求分析

  • 支持通过v-model控制显示/隐藏
  • 自定义标题、内容、底部按钮
  • 点击遮罩层关闭(可配置是否启用)
  • 入场/离场动画(淡入淡出)

步骤1:定义组件Modal.vue

vue 复制代码
<template>
  <Transition name="modal-fade">
    <div v-if="modelValue" class="modal-overlay" @click.self="handleClose">
      <div class="modal-container">
        <div class="modal-header">
          <h3>{{ title }}</h3>
          <button v-if="closable" @click="handleClose">×</button>
        </div>
        <div class="modal-body">
          <slot></slot> <!-- 默认插槽:主要内容 -->
        </div>
        <div class="modal-footer" v-if="showFooter">
          <slot name="footer"> <!-- 具名插槽:底部按钮 -->
            <button @click="handleClose">取消</button>
            <button type="primary" @click="handleConfirm">确认</button>
          </slot>
        </div>
      </div>
    </div>
  </Transition>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

// 定义Props(TypeScript)
const props = defineProps<{
  modelValue: boolean; // 控制显示(v-model绑定)
  title?: string; // 标题(可选)
  closable?: boolean; // 是否显示关闭按钮(默认true)
  showFooter?: boolean; // 是否显示底部(默认true)
  closeOnClickOverlay?: boolean; // 点击遮罩关闭(默认true)
}>();

// 定义Events
const emit = defineEmits<{
  (e: 'update:modelValue', value: boolean): void; // 更新v-model
  (e: 'close'): void; // 关闭事件
  (e: 'confirm'): void; // 确认事件
}>();

// 默认值处理
const handleClose = () => {
  if (props.closable !== false) {
    emit('update:modelValue', false);
    emit('close');
  }
};

const handleConfirm = () => {
  emit('confirm');
  handleClose();
};
</script>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-container {
  background: white;
  border-radius: 8px;
  width: 500px;
  max-width: 90%;
}

.modal-header {
  padding: 16px;
  border-bottom: 1px solid #eee;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.modal-body {
  padding: 16px;
  max-height: 60vh;
  overflow-y: auto;
}

.modal-footer {
  padding: 16px;
  border-top: 1px solid #eee;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

/* 动画效果 */
.modal-fade-enter-active,
.modal-fade-leave-active {
  transition: opacity 0.3s;
}
.modal-fade-enter-from,
.modal-fade-leave-to {
  opacity: 0;
}
</style>

步骤2:在父组件中使用

vue 复制代码
<script setup>
import { ref } from 'vue';
import Modal from './Modal.vue';

const isModalOpen = ref(false);

const openModal = () => {
  isModalOpen.value = true;
};

const handleModalClose = () => {
  console.log('弹窗已关闭');
};

const handleConfirm = () => {
  console.log('用户点击了确认');
};
</script>

<template>
  <button @click="openModal">打开弹窗</button>
  <Modal
    v-model="isModalOpen"
    title="提示"
    :closable="true"
    @close="handleModalClose"
    @confirm="handleConfirm"
  >
    <p>这是一段自定义内容</p>
    <!-- 自定义底部按钮 -->
    <template #footer>
      <button @click="isModalOpen = false">稍后处理</button>
      <button type="primary" @click="handleConfirm">立即确认</button>
    </template>
  </Modal>
</template>

关键知识点总结

  • v-model在组合式API中的实现:通过modelValue Prop和update:modelValue Event。
  • 作用域插槽:子组件通过<slot :item="data">传递数据,父组件用#default="{ item }"接收。
  • 过渡动画:使用Vue的<Transition>组件,通过CSS类名定义入场/离场效果。

第二部分 进阶篇:状态管理与工程化基础

第4章 Pinia:Vue 3的官方状态管理方案(详细)

4.1 为什么选择Pinia?(对比Vuex 4)

Vuex是Vue的经典状态管理库,但随着Vue 3的普及,其局限性逐渐凸显:

  • 代码冗余 :需编写statemutationsactions等多个对象,结构繁琐。
  • TypeScript支持弱:类型推断需手动声明,大型项目易出错。
  • 模块化不便 :模块间依赖需通过namespaced解决,不够直观。

Pinia作为Vuex的替代方案,针对上述问题做了优化,成为Vue 3的官方推荐状态管理库。

特性 Vuex 4 Pinia
API简洁性 需定义state/mutations/actions 仅需defineStore定义Store
TypeScript支持 需额外配置 原生支持,自动类型推断
模块化管理 依赖namespaced 天然模块化,按目录组织
组合式API风格 不支持 支持(与setup无缝集成)

4.2 核心概念与实战(附企业级案例)

4.2.1 Store的定义与使用

Pinia的核心是Store(存储),通过defineStore函数创建,每个Store可包含state(状态)、getters(计算属性)、actions(方法)。

示例:定义用户Store

javascript 复制代码
// stores/userStore.js
import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  // 状态(响应式数据)
  state: () => ({
    id: null,
    name: '',
    avatar: '',
    token: localStorage.getItem('token') || ''
  }),

  // Getters(计算属性,可访问state/actions)
  getters: {
    isLoggedIn: (state) => !!state.token,
    fullName: (state) => `${state.name}(ID: ${state.id})`
  },

  // Actions(方法,可同步/异步)
  actions: {
    // 登录(异步)
    async login(username, password) {
      try {
        const response = await fetch('/api/login', {
          method: 'POST',
          body: JSON.stringify({ username, password })
        });
        const data = await response.json();
        this.id = data.id;
        this.name = data.name;
        this.avatar = data.avatar;
        this.token = data.token;
        localStorage.setItem('token', data.token);
      } catch (error) {
        console.error('登录失败', error);
      }
    },

    // 登出(同步)
    logout() {
      this.id = null;
      this.name = '';
      this.avatar = '';
      this.token = '';
      localStorage.removeItem('token');
    }
  }
});

使用Store

vue 复制代码
<script setup>
import { useUserStore } from './stores/userStore';

const userStore = useUserStore();

// 访问状态
console.log(userStore.name); // 输出用户名

// 访问Getter
console.log(userStore.isLoggedIn); // 是否已登录

// 调用Action
userStore.login('admin', '123456');
</script>
4.2.2 组合式API风格与选项式API风格的对比

Pinia同时支持两种风格,但组合式API更推荐(与Vue 3的<script setup>更契合)。

组合式API风格(推荐)

javascript 复制代码
// stores/cartStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useCartStore = defineStore('cart', () => {
  // 状态(使用ref)
  const items = ref([]);

  // Getters(使用computed)
  const totalPrice = computed(() => {
    return items.value.reduce((sum, item) => sum + item.price * item.quantity, 0);
  });

  // Actions(方法)
  const addToCart = (product, quantity) => {
    const existingItem = items.value.find(item => item.id === product.id);
    if (existingItem) {
      existingItem.quantity += quantity;
    } else {
      items.value.push({ ...product, quantity });
    }
  };

  return { items, totalPrice, addToCart }; // 暴露给组件
});

选项式API风格(传统)

javascript 复制代码
// stores/productStore.js
import { defineStore } from 'pinia';

export const useProductStore = defineStore('product', {
  state: () => ({
    products: []
  }),
  getters: {
    expensiveProducts: (state) => state.products.filter(p => p.price > 1000)
  },
  actions: {
    async fetchProducts() {
      this.products = await fetch('/api/products').then(res => res.json());
    }
  }
});
4.2.3 多Store模块化管理

大型项目中,可将Store按功能拆分为多个文件(如userStorecartStoreproductStore),并通过storeToRefs解构保持响应式。

示例:在组件中使用多个Store

vue 复制代码
<script setup>
import { useUserStore } from './stores/userStore';
import { useCartStore } from './stores/cartStore';
import { storeToRefs } from 'pinia';

const userStore = useUserStore();
const cartStore = useCartStore();

// 解构State(保持响应式)
const { name } = storeToRefs(userStore);
const { items } = storeToRefs(cartStore);

// 直接访问Getters/Actions
console.log(cartStore.totalPrice);
cartStore.addToCart({ id: 1, name: '商品A', price: 99 }, 2);
</script>

4.3 实战:为待办事项应用添加全局状态管理

本实践将之前的待办事项列表从本地状态升级为Pinia全局状态,实现跨组件共享(如侧边栏显示待办总数)。

步骤1:定义todoStore

javascript 复制代码
// stores/todoStore.js
import { defineStore } from 'pinia';

export const useTodoStore = defineStore('todo', {
  state: () => ({
    todos: []
  }),

  getters: {
    remainingCount: (state) => state.todos.filter(todo => !todo.completed).length,
    completedCount: (state) => state.todos.filter(todo => todo.completed).length
  },

  actions: {
    addTodo(text) {
      this.todos.push({
        text,
        completed: false,
        id: Date.now() // 唯一ID
      });
    },

    removeTodo(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
    },

    toggleTodo(id) {
      const todo = this.todos.find(todo => todo.id === id);
      if (todo) todo.completed = !todo.completed;
    }
  }
});

步骤2:在侧边栏组件中显示统计信息

vue 复制代码
<!-- Sidebar.vue -->
<script setup>
import { useTodoStore } from '../stores/todoStore';
import { storeToRefs } from 'pinia';

const todoStore = useTodoStore();
const { remainingCount, completedCount } = storeToRefs(todoStore);
</script>

<template>
  <div class="sidebar">
    <h3>待办统计</h3>
    <p>剩余:{{ remainingCount }}</p>
    <p>已完成:{{ completedCount }}</p>
  </div>
</template>

步骤3:在待办列表组件中使用全局Store

vue 复制代码
<!-- TodoList.vue -->
<script setup>
import { useTodoStore } from '../stores/todoStore';

// 直接使用全局Store(无需通过props传递)
const todoStore = useTodoStore();

const newTodo = ref('');
const addTodo = () => {
  if (newTodo.value.trim()) {
    todoStore.addTodo(newTodo.value.trim());
    newTodo.value = '';
  }
};

const removeTodo = (id) => {
  todoStore.removeTodo(id);
};
</script>

关键知识点总结

  • Pinia的state是响应式的,直接修改即可触发视图更新(无需Vue.set)。
  • getters类似于计算属性,可依赖其他stategetters,支持缓存。
  • actions用于封装复杂逻辑(如异步请求),可通过this访问Store实例。

第三部分 实战篇:完整项目开发与优化

第7章 企业级项目实战:电商后台管理系统(详细)

7.1 需求分析与技术选型(附原型图说明)

项目背景:某电商公司需要开发一个后台管理系统,用于商品管理、订单处理、用户数据统计等核心业务。

核心功能模块

  • 商品管理:商品列表、添加/编辑商品、上下架
  • 订单管理:订单列表、详情查看、发货/退款处理
  • 用户管理:用户列表、权限设置、数据导出
  • 数据看板:销售额统计、订单趋势、热门商品

技术选型

  • 前端框架:Vue 3 + TypeScript(类型安全、大型项目友好)
  • UI组件库:Element Plus(丰富的后台组件,如表格、表单、弹窗)
  • 状态管理:Pinia(替代Vuex,更简洁的API)
  • 路由:Vue Router 4(动态路由、权限控制)
  • 图表:ECharts(数据可视化)
  • HTTP客户端:Axios(拦截器、请求封装)
  • 构建工具:Vite(快速的开发体验)

7.2 项目搭建与架构设计(附目录结构规范)

7.2.1 初始化项目

使用create-vue创建项目,勾选以下配置:

  • TypeScript
  • Pinia
  • Vue Router
  • ESLint + Prettier(代码规范)
  • ESLint-plugin-vue(Vue语法检查)
bash 复制代码
npm create vue@latest ecom-admin
# 按提示选择配置后,进入项目目录
cd ecom-admin
pnpm install
# 安装Element Plus
pnpm add element-plus
# 安装Axios
pnpm add axios
7.2.2 目录结构规范(关键目录说明)
csharp 复制代码
ecom-admin/
├── public/               # 静态资源(logo、favicon)
├── src/
│   ├── api/              # API请求封装(按模块划分)
│   │   ├── product.ts    # 商品相关API
│   │   ├── order.ts      # 订单相关API
│   │   └── user.ts       # 用户相关API
│   ├── assets/           # 静态资源(图片、样式)
│   ├── components/       # 公共组件(全局注册)
│   │   ├── Layout/       # 布局组件(侧边栏、头部)
│   │   ├── Table/        # 增强版表格组件
│   │   └── Form/         # 增强版表单组件
│   ├── hooks/            # 自定义Hook(逻辑复用)
│   │   ├── useAuth.ts    # 权限校验Hook
│   │   └── useRequest.ts # 请求封装Hook
│   ├── router/           # 路由配置(动态路由、权限控制)
│   ├── stores/           # Pinia Store(按模块划分)
│   │   ├── app.ts        # 全局状态(主题、加载状态)
│   │   ├── product.ts    # 商品状态
│   │   └── user.ts       # 用户状态
│   ├── utils/            # 工具函数(日期格式化、权限判断)
│   ├── views/            # 页面组件(按模块划分)
│   │   ├── product/      # 商品管理页面
│   │   ├── order/        # 订单管理页面
│   │   └── dashboard/    # 数据看板页面
│   ├── App.vue           # 根组件(布局容器)
│   └── main.ts           # 入口文件(初始化全局样式、插件)
├── .eslintrc.js          # ESLint配置
├── vite.config.ts        # Vite配置(别名、插件)
├── tsconfig.json         # TypeScript配置
└── package.json          # 依赖管理
7.2.3 全局配置(主题、请求拦截、错误处理)

主题定制 :Element Plus支持自定义主题,通过vite-plugin-element-plus自动导入样式。

typescript 复制代码
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import ElementPlus from 'unplugin-element-plus/vite';

export default defineConfig({
  plugins: [
    vue(),
    ElementPlus({
      // 自定义主题色(修改SCSS变量)
      useSource: true,
      // 按需导入(减小打包体积)
      importStyle: 'scss'
    })
  ]
});

Axios请求封装:统一处理请求头、错误提示、加载状态。

typescript 复制代码
// utils/request.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { ElMessage } from 'element-plus';
import { useAppStore } from '../stores/app';

const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量获取
  timeout: 10000
});

// 请求拦截器:添加Token
service.interceptors.request.use((config: AxiosRequestConfig) => {
  const appStore = useAppStore();
  if (appStore.token) {
    config.headers = {
      ...config.headers,
      Authorization: `Bearer ${appStore.token}`
    };
  }
  return config;
});

// 响应拦截器:处理错误码
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const res = response.data;
    if (res.code !== 200) {
      ElMessage.error(res.message || '请求失败');
      // 401:未登录/Token过期
      if (res.code === 401) {
        // 跳转到登录页
      }
      return Promise.reject(new Error(res.message));
    }
    return res.data;
  },
  (error) => {
    ElMessage.error(error.message || '网络异常');
    return Promise.reject(error);
  }
);

export default service;

7.3 核心功能开发(附关键代码)

7.3.1 商品列表页(分页、筛选、排序)

需求:展示商品列表,支持按名称搜索、价格区间筛选、销量排序,分页加载。

实现步骤

  1. API接口定义src/api/product.ts):

    typescript 复制代码
    import request from '../utils/request';
    
    export interface Product {
      id: number;
      name: string;
      price: number;
      stock: number;
      sales: number;
      status: 0 | 1; // 0-下架,1-上架
    }
    
    export interface QueryParams {
      page: number;
      pageSize: number;
      keyword?: string;
      minPrice?: number;
      maxPrice?: number;
      sortBy?: 'sales' | 'price';
      sortOrder?: 'asc' | 'desc';
    }
    
    export function getProductList(params: QueryParams) {
      return request({
        url: '/api/products',
        method: 'GET',
        params
      });
    }
  2. 页面组件开发src/views/product/List.vue):

    vue 复制代码
    <script setup>
    import { ref, reactive, onMounted } from 'vue';
    import { getProductList } from '@/api/product';
    import { ElTable, ElPagination, ElInput, ElSelect, ElOption } from 'element-plus';
    
    // 状态
    const tableData = ref<Product[]>([]);
    const loading = ref(false);
    const total = ref(0);
    
    // 查询参数
    const queryParams = reactive<QueryParams>({
      page: 1,
      pageSize: 10,
      sortBy: 'sales',
      sortOrder: 'desc'
    });
    
    // 搜索关键字
    const keyword = ref('');
    
    // 加载数据
    const loadData = async () => {
      loading.value = true;
      try {
        const res = await getProductList({ ...queryParams, keyword: keyword.value });
        tableData.value = res.list;
        total.value = res.total;
      } catch (error) {
        console.error('加载失败', error);
      } finally {
        loading.value = false;
      }
    };
    
    // 页码变化
    const handlePageChange = (page: number) => {
      queryParams.page = page;
      loadData();
    };
    
    // 每页数量变化
    const handleSizeChange = (size: number) => {
      queryParams.pageSize = size;
      queryParams.page = 1;
      loadData();
    };
    
    // 搜索
    const handleSearch = () => {
      queryParams.page = 1;
      loadData();
    };
    
    onMounted(loadData);
    </script>
    
    <template>
      <div class="product-list">
        <div class="search-bar">
          <ElInput
            v-model="keyword"
            placeholder="输入商品名称搜索"
            style="width: 300px"
            clearable
          />
          <ElButton type="primary" @click="handleSearch">搜索</ElButton>
        </div>
    
        <ElTable :data="tableData" border v-loading="loading">
          <ElTableColumn prop="name" label="商品名称" />
          <ElTableColumn prop="price" label="价格" width="100" />
          <ElTableColumn prop="stock" label="库存" width="100" />
          <ElTableColumn prop="sales" label="销量" width="100" />
          <ElTableColumn prop="status" label="状态" width="100">
            <template #default="{ row }">
              <ElTag :type="row.status === 1 ? 'success' : 'info'">
                {{ row.status === 1 ? '上架' : '下架' }}
              </ElTag>
            </template>
          </ElTableColumn>
        </ElTable>
    
        <ElPagination
          :current-page="queryParams.page"
          :page-size="queryParams.pageSize"
          :total="total"
          layout="total, sizes, prev, pager, next"
          @size-change="handleSizeChange"
          @current-change="handlePageChange"
        />
      </div>
    </template>
7.3.2 订单详情页(动态路由传参、富文本编辑器)

需求:点击订单列表的"查看详情"跳转到详情页,展示订单的收货地址、商品明细、物流信息,并支持编辑备注(使用富文本编辑器)。

实现步骤

  1. 动态路由配置src/router/index.ts):

    typescript 复制代码
    import { createRouter, createWebHistory } from 'vue-router';
    
    const router = createRouter({
      history: createWebHistory(),
      routes: [
        // 其他路由...
        {
          path: '/order/:orderId',
          name: 'OrderDetail',
          component: () => import('@/views/order/Detail.vue'),
          meta: { title: '订单详情', requiresAuth: true }
        }
      ]
    });
    
    export default router;
  2. 富文本编辑器集成 (使用@wangeditor/editor-for-vue):

    bash 复制代码
    pnpm add @wangeditor/editor-for-vue
  3. 订单详情页组件src/views/order/Detail.vue):

    vue 复制代码
    <script setup>
    import { ref, onMounted } from 'vue';
    import { useRoute } from 'vue-router';
    import { ElMessage } from 'element-plus';
    import { getOrderBy

7.3.3 订单详情页(动态路由传参、富文本编辑器集成)(续)

typescript 复制代码
// 续:订单详情页组件
import { getOrderByID, updateOrderRemark } from '@/api/order';
import { Editor, createToolbar } from '@wangeditor/editor-for-vue';

const route = useRoute();
const orderId = route.params.orderId as string;

// 订单详情数据
const orderDetail = ref({
  id: '',
  orderNo: '',
  createTime: '',
  totalAmount: 0,
  status: '',
  address: {} as Address,
  items: [] as OrderItem[],
  remark: ''
});

// 加载订单详情
const loadOrderDetail = async () => {
  try {
    const res = await getOrderByID(orderId);
    orderDetail.value = res.data;
  } catch (error) {
    ElMessage.error('获取订单详情失败');
  }
};

// 编辑备注
const handleRemarkChange = (editor: any) => {
  orderDetail.value.remark = editor.getHtml();
};

// 保存备注
const saveRemark = async () => {
  try {
    await updateOrderRemark(orderId, { remark: orderDetail.value.remark });
    ElMessage.success('备注保存成功');
  } catch (error) {
    ElMessage.error('保存失败');
  }
};

onMounted(loadOrderDetail);
</script>

<template>
  <div class="order-detail">
    <h2>订单详情:{{ orderDetail.orderNo }}</h2>

    <!-- 基础信息 -->
    <ElCard class="section">
      <template #header>基础信息</template>
      <ElDescriptions :column="2">
        <ElDescriptionsItem label="下单时间">
          {{ formatDate(orderDetail.createTime) }}
        </ElDescriptionsItem>
        <ElDescriptionsItem label="订单状态">
          <ElTag :type="getStatusType(orderDetail.status)">
            {{ getStatusText(orderDetail.status) }}
          </ElTag>
        </ElDescriptionsItem>
        <ElDescriptionsItem label="支付金额">
          ¥{{ orderDetail.totalAmount.toFixed(2) }}
        </ElDescriptionsItem>
        <ElDescriptionsItem label="收货地址">
          {{ formatAddress(orderDetail.address) }}
        </ElDescriptionsItem>
      </ElDescriptions>
    </ElCard>

    <!-- 商品明细 -->
    <ElCard class="section">
      <template #header>商品明细</template>
      <ElTable :data="orderDetail.items" border>
        <ElTableColumn prop="productName" label="商品名称" />
        <ElTableColumn prop="price" label="单价" width="100">
          ¥{{ props.row.price.toFixed(2) }}
        </ElTableColumn>
        <ElTableColumn prop="quantity" label="数量" width="100" />
        <ElTableColumn label="小计" width="120">
          ¥{{ (props.row.price * props.row.quantity).toFixed(2) }}
        </ElTableColumn>
      </ElTable>
    </ElCard>

    <!-- 备注编辑 -->
    <ElCard class="section">
      <template #header>备注信息</template>
      <div class="editor-container">
        <Editor
          v-model="orderDetail.remark"
          :defaultConfig="editorConfig"
          mode="default"
          @onCreated="onEditorCreated"
        />
        <ElButton type="primary" @click="saveRemark">保存备注</ElButton>
      </div>
    </ElCard>
  </div>
</template>

<style scoped>
.section {
  margin-bottom: 20px;
}
.editor-container {
  margin-top: 10px;
}
</style>

关键技术点解析

  • 动态路由传参 :通过route.params.orderId获取动态参数,结合useRoute钩子实现页面跳转时的数据传递。
  • 富文本编辑器集成 :使用@wangeditor/editor-for-vue组件,通过v-model绑定内容,onCreated生命周期初始化编辑器。
  • 响应式数据更新 :订单详情数据通过ref声明,确保视图随数据变化自动更新。
7.3.4 数据看板(ECharts集成、WebSocket实时更新)

需求:展示销售额趋势、订单量统计、热门商品排行,支持实时刷新(每30秒更新一次)。

实现步骤

  1. ECharts集成 (安装echartsvue-echarts):

    bash 复制代码
    pnpm add echarts vue-echarts
  2. 数据看板组件src/views/dashboard/Index.vue):

    vue 复制代码
    <script setup>
    import { ref, onMounted, onUnmounted } from 'vue';
    import { useDark } from '@vueuse/core';
    import { getSalesTrend, getOrderStats, getHotProducts } from '@/api/dashboard';
    import { useWebSocket } from '@/hooks/useWebSocket'; // 自定义WebSocket Hook
    
    // 图表实例
    const salesChart = ref<echarts.ECharts>();
    const orderChart = ref<echarts.ECharts>();
    const hotProductsChart = ref<echarts.ECharts>();
    
    // 数据
    const salesTrend = ref([]);
    const orderStats = ref({ today: 0, yesterday: 0 });
    const hotProducts = ref([]);
    
    // WebSocket连接(实时更新)
    const { message } = useWebSocket('ws://your-api.com/dashboard/realtime');
    
    // 初始化图表
    const initCharts = () => {
      // 销售额趋势图
      salesChart.value = echarts.init(document.getElementById('sales-trend'));
      salesChart.value.setOption({
        title: { text: '销售额趋势' },
        tooltip: { trigger: 'axis' },
        xAxis: { type: 'category', data: salesTrend.value.map(item => item.date) },
        yAxis: { type: 'value' },
        series: [{ name: '销售额', type: 'line', data: salesTrend.value.map(item => item.amount) }]
      });
    
      // 其他图表初始化类似...
    };
    
    // 获取数据
    const loadData = async () => {
      const [salesRes, orderRes, productsRes] = await Promise.all([
        getSalesTrend(),
        getOrderStats(),
        getHotProducts()
      ]);
      salesTrend.value = salesRes.data;
      orderStats.value = orderRes.data;
      hotProducts.value = productsRes.data;
      updateCharts();
    };
    
    // 更新图表数据
    const updateCharts = () => {
      if (salesChart.value) {
        salesChart.value.setOption({
          xAxis: { data: salesTrend.value.map(item => item.date) },
          series: [{ data: salesTrend.value.map(item => item.amount) }]
        });
      }
      // 其他图表更新类似...
    };
    
    // WebSocket消息处理
    message.value?.onmessage = (event) => {
      const data = JSON.parse(event.data);
      if (data.type === 'sales') {
        salesTrend.value.unshift(data.item);
        updateCharts();
      }
    };
    
    onMounted(() => {
      loadData();
      initCharts();
      // 定时刷新(30秒)
      const timer = setInterval(loadData, 30000);
      onUnmounted(() => clearInterval(timer));
    });
    </script>
    
    <template>
      <div class="dashboard">
        <h2>数据看板</h2>
    
        <!-- 核心指标 -->
        <ElRow :gutter="20">
          <ElCol :span="8">
            <ElCard>
              <template #header>今日销售额</template>
              <div class="stat-value">¥{{ orderStats.today.toFixed(2) }}</div>
            </ElCard>
          </ElCol>
          <ElCol :span="8">
            <ElCard>
              <template #header>今日订单量</template>
              <div class="stat-value">{{ orderStats.today }}</div>
            </ElCard>
          </ElCol>
          <ElCol :span="8">
            <ElCard>
              <template #header>热门商品</template>
              <div class="stat-value">{{ hotProducts[0]?.name }}</div>
            </ElCard>
          </ElCol>
        </ElRow>
    
        <!-- 图表区域 -->
        <ElRow :gutter="20" style="margin-top: 20px">
          <ElCol :span="12">
            <div id="sales-trend" style="width: 100%; height: 400px"></div>
          </ElCol>
          <ElCol :span="12">
            <div id="order-stats" style="width: 100%; height: 400px"></div>
          </ElCol>
        </ElRow>
      </div>
    </template>
    
    <style scoped>
    .stat-value {
      font-size: 24px;
      font-weight: bold;
      color: var(--el-color-primary);
    }
    </style>

关键技术点解析

  • ECharts响应式 :通过vue-echarts组件绑定数据,结合resize事件监听窗口变化,自动调整图表尺寸。
  • WebSocket实时更新 :自定义useWebSocket Hook封装连接逻辑,通过onmessage事件处理实时数据推送。
  • 性能优化 :使用Promise.all并行请求数据,减少加载时间;定时器在组件卸载时清除,避免内存泄漏。

第8章 性能优化:从加载到运行的全链路优化(详细)

8.1 首屏加载优化(附Lighthouse优化前后对比)

8.1.1 代码分割与懒加载

Vue 3配合Vite默认支持ES模块的动态导入,可通过import()实现组件、路由、第三方库的懒加载。

实践:路由懒加载

typescript 复制代码
// router/index.ts
const routes = [
  {
    path: '/product',
    name: 'Product',
    component: () => import('@/views/product/List.vue'), // 动态导入
    children: [
      {
        path: 'detail/:id',
        name: 'ProductDetail',
        component: () => import('@/views/product/Detail.vue') // 嵌套路由懒加载
      }
    ]
  }
];

第三方库按需引入

使用unplugin-auto-import自动导入Vue API(如refcomputed),减少手动导入的代码量;

使用unplugin-vue-components自动导入Element Plus组件(如ElButtonElTable),避免全局注册的冗余。

typescript 复制代码
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';

export default defineConfig({
  plugins: [
    // ...其他插件
    AutoImport({
      imports: ['vue', 'vue-router'],
      dts: 'src/auto-imports.d.ts'
    }),
    Components({
      resolvers: [ElementPlusResolver()],
      dirs: ['src/components'], // 扫描本地组件
      dts: 'src/components.d.ts'
    })
  ]
});
8.1.2 资源优化
  • 图片压缩与处理 :使用image-webpack-loader压缩图片,vite-plugin-imagemin自动优化;
  • 字体子集化:仅保留项目中使用的字符(如中文常用字),减少字体文件体积;
  • CDN加速 :将静态资源(Vue、Element Plus、ECharts)托管到CDN,通过vite.config.ts配置base路径。
typescript 复制代码
// vite.config.ts
export default defineConfig({
  base: 'https://cdn.example.com/', // CDN基础路径
  build: {
    assetsInlineLimit: 0, // 强制外部化资源
    rollupOptions: {
      output: {
        assetFileNames: '[name]-[hash].[ext]' // 自定义资源文件名
      }
    }
  }
});
8.1.3 Lighthouse优化实践

通过Chrome DevTools的Lighthouse工具检测,针对以下指标优化:

指标 目标值 优化方法
首次内容渲染(FCP) <1.5s 代码分割、CDN加速、预加载关键资源
最大内容渲染(LCP) <2.5s 图片懒加载、优化图片尺寸、服务端渲染
总阻塞时间(TBT) <300ms 减少长任务、优化JavaScript执行
累积布局偏移(CLS) <0.1 为动态内容预留空间、避免无占位符的图片

8.2 运行时性能优化

8.2.1 响应式优化

Vue 3的响应式系统虽高效,但不当使用仍可能导致性能问题:

  • 避免过度响应式 :对于不需要响应式的大对象(如配置项),使用shallowRefmarkRaw标记。

    typescript 复制代码
    const config = shallowRef({ theme: 'light', layout: 'grid' }); // 仅第一层响应式
    const rawUser = markRaw(user); // 标记为原始对象,不跟踪依赖
  • 减少依赖收集 :在computedwatch中,避免引用不必要的大对象。

8.2.2 虚拟滚动

对于大数据量列表(如1000+条数据),使用vue-virtual-scroller组件仅渲染可见区域的DOM节点。

实践:集成虚拟滚动

bash 复制代码
pnpm add vue-virtual-scroller
vue 复制代码
<template>
  <RecycleScroller
    class="list-container"
    :items="items"
    :item-size="50"
    key-field="id"
  >
    <template #default="{ item }">
      <div class="list-item">{{ item.name }}</div>
    </template>
  </RecycleScroller>
</template>

<script setup>
import { ref } from 'vue';
import { RecycleScroller } from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';

const items = ref(Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `商品${i}` })));
</script>
8.2.3 防抖与节流

高频事件(如搜索框输入、窗口缩放)需使用防抖(Debounce)或节流(Throttle)减少函数执行次数。

实践:自定义防抖Hook

typescript 复制代码
// hooks/useDebounce.ts
import { ref, onUnmounted } from 'vue';

export function useDebounce(fn: Function, delay = 500) {
  let timer: NodeJS.Timeout;
  const debouncedFn = (...args: any[]) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
  onUnmounted(() => clearTimeout(timer));
  return debouncedFn;
}

// 使用示例
const searchInput = ref('');
const search = useDebounce(async (keyword) => {
  const res = await getProductList({ keyword });
  // 更新数据...
}, 500);

// 模板中绑定
<input v-model="searchInput" @input="search(searchInput)" />

第9章 工程化进阶:测试与部署(详细)

9.1 单元测试与E2E测试

9.1.1 单元测试(Vitest配置)

Vitest是Vue官方推荐的测试框架,支持与Vite无缝集成,兼容Jest语法。

配置步骤

  1. 安装依赖:

    bash 复制代码
    pnpm add vitest @vue/test-utils jsdom --save-dev
  2. 配置vitest.config.ts

    typescript 复制代码
    import { defineConfig } from 'vitest/config';
    import vue from '@vitejs/plugin-vue';
    
    export default defineConfig({
      plugins: [vue()],
      test: {
        environment: 'jsdom', // 模拟浏览器环境
        coverage: {
          provider: 'v8', // 代码覆盖率
          reporter: ['text', 'html']
        }
      }
    });
  3. 编写测试用例(src/components/TodoList.spec.ts):

    typescript 复制代码
    import { describe, it, expect, vi } from 'vitest';
    import { mount } from '@vue/test-utils';
    import TodoList from '../TodoList.vue';
    
    describe('TodoList', () => {
      it('添加待办事项', async () => {
        const wrapper = mount(TodoList);
        const input = wrapper.find('input');
        const button = wrapper.find('button');
    
        // 模拟输入和点击
        await input.setValue('学习Vue 3');
        await button.trigger('click');
    
        // 断言列表包含新项
        expect(wrapper.findAll('li')).toHaveLength(1);
        expect(wrapper.text()).toContain('学习Vue 3');
      });
    });
9.1.2 E2E测试(Cypress集成)

Cypress是功能强大的E2E测试工具,支持模拟用户操作、验证页面行为。

配置步骤

  1. 安装依赖:

    bash 复制代码
    pnpm add cypress --save-dev
  2. 初始化Cypress:

    bash 复制代码
    npx cypress open
  3. 编写测试用例(cypress/e2e/dashboard.cy.ts):

    typescript 复制代码
    describe('数据看板', () => {
      it('显示今日销售额', () => {
        // 访问页面
        cy.visit('/dashboard');
        
        // 等待数据加载
        cy.get('[data-testid="sales-value"]').should('contain', '¥');
        
        // 验证数值大于0
        cy.get('[data-testid="sales-value"]').invoke('text')
          .then(text => {
            const amount = parseFloat(text.replace('¥', ''));
            expect(amount).to.be.greaterThan(0);
          });
      });
    });
  4. 配置CI/CD自动运行测试(GitHub Actions):

    yaml 复制代码
    # .github/workflows/test.yml
    name: Test
    on: [push]
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with: { node-version: 20 }
          - run: npm ci
          - run: npm run test:unit # Vitest单元测试
          - run: npm run test:e2e # Cypress E2E测试(需启动服务)

9.2 生产环境部署

9.2.1 Docker容器化

将Vue项目打包为Docker镜像,便于部署到云服务器(如阿里云、AWS)。

Dockerfile示例

dockerfile 复制代码
# 基础镜像(Node.js 18)
FROM node:18-alpine as builder

# 设置工作目录
WORKDIR /app

# 复制依赖文件并安装
COPY package*.json ./
RUN npm ci --omit=dev

# 复制项目代码并构建
COPY . .
RUN npm run build

# 生产镜像(Nginx)
FROM nginx:alpine

# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html

# 配置Nginx
COPY nginx.conf /etc/nginx/conf.d/default.conf

# 启动Nginx
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Nginx配置(nginx.conf

conf 复制代码
server {
  listen 80;
  server_name your-domain.com;

  location / {
    root /usr/share/nginx/html;
    index index.html;
    try_files $uri $uri/ /index.html; # 支持前端路由
  }

  # 反向代理API请求
  location /api {
    proxy_pass http://your-api-server:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }
}
9.2.2 CI/CD流程(GitHub Actions)

通过GitHub Actions实现代码推送后自动构建、测试、部署。

yaml 复制代码
# .github/workflows/deploy.yml
name: Deploy
on: [push]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: 构建项目
        run: |
          npm ci
          npm run build
      
      - name: 构建Docker镜像
        uses: docker/build-push-action@v5
        with:
          context: .
          push: false # 本地测试,不推送到仓库
          tags: my-vue-app:latest
      
      - name: 部署到服务器
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            # 停止旧容器
            docker stop my-vue-app || true
            docker rm my-vue-app || true
            # 启动新容器
            docker run -d -p 80:80 --name my-vue-app my-vue-app:latest

第10章 开发者软技能:代码规范与团队协作(详细)

10.1 代码规范(ESLint + Prettier配置)

10.1.1 ESLint配置(Vue 3 + TypeScript)

通过ESLint统一代码风格,结合Prettier格式化代码,避免"格式战争"。

.eslintrc.js配置

javascript 复制代码
module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
    es2021: true
  },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended' // 与Prettier集成
  ],
  parser: 'vue-eslint-parser',
  parserOptions: {
    ecmaVersion: 'latest',
    parser: '@typescript-eslint/parser',
    sourceType: 'module'
  },
  rules: {
    'vue/multi-word-component-names': 'off', // 关闭组件名多单词限制
    '@typescript-eslint/no-explicit-any': 'warn', // 禁止any类型
    'prettier/prettier': 'error' // Prettier格式化错误视为ESLint错误
  }
};
10.1.2 Prettier配置(.prettierrc)
json 复制代码
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100,
  "endOfLine": "lf"
}
10.1.3 保存自动格式化(VS Code配置)

.vscode/settings.json中添加:

json 复制代码
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}

10.2 文档与协作

10.2.1 组件库文档(Storybook)

使用Storybook为公共组件编写交互式文档,方便团队成员查看组件用法。

安装与配置

bash 复制代码
pnpm add storybook @storybook/vue3 @storybook/addon-essentials --save-dev

创建故事文件src/components/Button.stories.mdx):

markdown 复制代码
import { Button } from './Button.vue';

# Button 组件

基础按钮组件,支持主色、次色、危险色样式。

## 示例

<Canvas>
  <Story name="基础用法">
    <Button type="primary">主要按钮</Button>
    <Button type="default">默认按钮</Button>
    <Button type="danger">危险按钮</Button>
  </Story>
</Canvas>

## Props

| Prop  | 类型   | 默认值 | 说明       |
|-------|--------|--------|------------|
| type  | string | 'default' | 按钮类型:primary/default/danger |
| size  | string | 'medium' | 按钮尺寸:small/medium/large |
| disabled | boolean | false | 是否禁用 |

启动Storybook

bash 复制代码
pnpm storybook
10.2.2 接口文档(Swagger)

后端与前端协作时,使用Swagger生成API文档,明确请求参数、响应格式。

后端配置示例(Node.js + Express)

javascript 复制代码
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: '电商后台管理系统API',
      version: '1.0.0'
    }
  },
  apis: ['./routes/*.ts'] // 扫描路由文件
};

const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));

前端访问文档

通过http://your-api.com/api-docs访问交互式API文档,查看接口详情并测试请求。

10.3 经验分享:常见踩坑总结

10.3.1 响应式丢失问题

现象 :修改数组或对象的属性时,视图未更新。
原因 :Vue 3的响应式系统基于Proxy,但对数组的索引赋值、对象新增属性等情况无法自动追踪。
解决方案

  • 数组:使用splice方法或替换整个数组(this.items = [...newItems])。
  • 对象:使用Vue.set(Vue 2)或set函数(Vue 3,需从@vue/reactivity导入)。
typescript 复制代码
// Vue 3中正确修改对象属性
import { set } from '@vue/reactivity';

const obj = ref({ name: '张三' });
set(obj.value, 'age', 20); // 触发响应式更新
10.3.2 内存泄漏问题

现象 :组件卸载后,定时器或事件监听器仍执行,导致页面卡顿。
原因 :未在组件卸载时清理副作用(如setTimeoutaddEventListener)。
解决方案

  • 使用onUnmounted钩子清理定时器。
  • 使用once选项监听一次性事件。
typescript 复制代码
// 组件中清理定时器
import { onUnmounted } from 'vue';

const timer = setInterval(() => {
  console.log('定时器执行');
}, 1000);

onUnmounted(() => {
  clearInterval(timer);
});
10.3.3 跨域问题

现象 :前端请求后端API时,浏览器报"Access to XMLHttpRequest at 'api.com' from origin 'http://localhost:5173' has been blocked by CORS policy"。
原因 :后端未配置CORS(跨域资源共享)头。
解决方案

  • 后端添加CORS头(如Express的cors中间件)。
  • 前端开发环境配置代理(Vite的vite.config.ts):
typescript 复制代码
// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://your-api.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
});

附录

附录A:Vue 3官方文档速查表

附录B:常用工具库推荐

类别 工具库 说明
UI组件库 Element Plus 后台管理系统首选,支持Vue 3
HTTP客户端 Axios 支持拦截器、请求取消
图表库 ECharts 功能强大的数据可视化库
状态管理 Pinia Vue 3官方推荐,替代Vuex
表单校验 VeeValidate 灵活的表单验证解决方案

附录C:学习资源推荐

  • 书籍:《Vue.js设计与实现》(霍春阳)、《Vue 3从入门到精通》(明日科技)
  • 视频 :Vue官方教程(vuejs.org/zh/tutorial... 3核心源码解析》
  • 社区 :Vue GitHub仓库(github.com/vuejs/core)... Overflow(标签:vue.js)

后记

Vue.js的魅力在于其"渐进式"的设计哲学------你可以从一个简单的单页面应用开始,逐步引入路由、状态管理、工程化工具,最终构建出复杂的企业级系统。本书的结束并非学习的终点,而是你探索Vue世界的起点。希望你能保持对技术的热情,在实践中不断积累经验,成为一名优秀的前端开发者。

愿你在Vue的星空中,找到属于自己的那颗最亮的星! 🌟

相关推荐
啃火龙果的兔子4 分钟前
安全有效的 C 盘清理方法
前端·css
海天胜景8 分钟前
vue3 数据过滤方法
前端·javascript·vue.js
天生我材必有用_吴用13 分钟前
深入理解JavaScript设计模式之策略模式
前端
海上彼尚15 分钟前
Vue3 PC端 UI组件库我更推荐Naive UI
前端·vue.js·ui
述雾学java15 分钟前
Vue 生命周期详解(重点:mounted)
前端·javascript·vue.js
洛千陨21 分钟前
Vue实现悬浮图片弹出大图预览弹窗,弹窗顶部与图片顶部平齐
前端·vue.js
咚咚咚ddd22 分钟前
微前端第四篇:qiankun老项目渐进式升级方案(jQuery + React)
前端·前端工程化
螃蟹82725 分钟前
作用域下的方法如何调用?
前端
独立开阀者_FwtCoder28 分钟前
TypeScript 杀疯了,开发 AI 应用新趋势!
前端·javascript·github
汪子熙34 分钟前
QRCode.js:一款轻量级、跨浏览器的 JavaScript 二维码生成库
前端·javascript·面试