Vue相关

43 minute read

Vue.js 框架 完整系统学习大纲


一、Vue.js 概述

1.1 Vue.js 发展历史

  • 2013年:尤雨溪在 Google 工作时创建 Vue.js
  • 2014年2月:正式发布 Vue.js 0.x
  • 2015年10月:Vue.js 1.0 发布
  • 2016年10月:Vue.js 2.0 发布,引入虚拟 DOM
  • 2019年9月:Vue.js 3.0 Beta 版发布
  • 2020年9月:Vue.js 3.0 正式版发布
  • 2022年1月:Vue.js 3 成为新的默认版本
  • 2023年:Vue 3.3+ 发布,TypeScript 支持大幅增强

1.2 Vue.js 核心特性

  • 响应式系统:基于 ES6 Proxy 的数据绑定
  • 组件化:可复用、可组合的 UI 组件
  • 虚拟 DOM:高效的 DOM 更新机制
  • 指令系统:v-if、v-for、v-bind 等
  • 模板语法:声明式渲染
  • 组合式 API:Vue 3 新特性,更好的逻辑复用
  • 渐进式框架:可按需引入功能

1.3 Vue.js 生态系统

  • 核心库:Vue.js
  • 路由:Vue Router
  • 状态管理:Pinia(推荐)、Vuex
  • 构建工具:Vite、Vue CLI
  • UI 框架:Element Plus、Ant Design Vue、Vuetify
  • 服务端渲染:Nuxt.js
  • 移动端:Ionic Vue
  • 桌面端:Electron + Vue
  • 测试工具:Vue Test Utils、Vitest

1.4 Vue 3 与 Vue 2 对比

特性 Vue 2 Vue 3
响应式系统 Object.defineProperty Proxy
组件实例 单个 this 实例 多个 setup 上下文
代码组织 Options API Composition API + Options API
TypeScript 支持 一般 优秀
包大小 较大 更小(Tree-shaking)
性能 良好 更好(编译优化)
生命周期 beforeCreate, created 等 新增 setup, onBeforeMount 等

1.5 第一个 Vue 应用

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
    <div id="app">
        <h1> {{  message }} </h1>
        <p>计数:  {{  count }} </p>
        <button @click="increment">增加</button>
    </div>

    <script>
        const { createApp, ref } = Vue;
        
        createApp({
            setup() {
                const message = ref('Hello Vue 3!');
                const count = ref(0);
                
                const increment = () => {
                    count.value++;
                };
                
                return {
                    message,
                    count,
                    increment
                };
            }
        }).mount('#app');
    </script>
</body>
</html>

二、Vue 基础

2.1 Vue 实例与应用

// 创建 Vue 应用
import { createApp } from 'vue';

// 根组件
const App = {
    data() {
        return {
            message: 'Hello Vue!'
        };
    }
};

// 创建应用实例
const app = createApp(App);

// 全局配置
app.config.errorHandler = (err, vm, info) => {
    console.error('Vue 错误:', err, info);
};

// 全局属性
app.config.globalProperties.$myGlobal = '全局属性';

// 全局组件
app.component('MyComponent', {
    template: '<div>全局组件</div>'
});

// 全局指令
app.directive('focus', {
    mounted(el) {
        el.focus();
    }
});

// 挂载到 DOM
app.mount('#app');

// 应用实例方法
app.component();  // 注册组件
app.directive();  // 注册指令
app.use();        // 使用插件
app.provide();    // 提供依赖
app.mount();      // 挂载应用
app.unmount();    // 卸载应用
app.version;      // Vue 版本

2.2 模板语法

<template>
    <!-- 1. 插值表达式 -->
    <p> {{  message }} </p>
    <p> {{  number + 1 }} </p>
    <p> {{  ok ? 'YES' : 'NO' }} </p>
    <p> {{  message.split('').reverse().join('') }} </p>
    
    <!-- 2. 原始 HTML -->
    <p>Using mustaches:  {{  rawHtml }} </p>
    <p>Using v-html directive: <span v-html="rawHtml"></span></p>
    
    <!-- 3. 属性绑定 -->
    <div v-bind:id="dynamicId"></div>
    <div :id="dynamicId"></div>  <!-- 简写 -->
    <button :disabled="isButtonDisabled">按钮</button>
    
    <!-- 4. JavaScript 表达式 -->
     {{  number + 1 }} 
     {{  ok ? 'YES' : 'NO' }} 
     {{  message.split('').reverse().join('') }} 
    
    <!-- 5. 动态参数 -->
    <a v-bind:[attributeName]="url">链接</a>
    <a :[attributeName]="url">链接</a>  <!-- 简写 -->
    
    <!-- 6. 修饰符 -->
    <form @submit.prevent="onSubmit">表单</form>
</template>

2.3 计算属性

// Options API
export default {
    data() {
        return {
            firstName: '',
            lastName: '',
            items: [
                { id: 1, name: '苹果', price: 5 },
                { id: 2, name: '香蕉', price: 3 },
                { id: 3, name: '橙子', price: 4 }
            ]
        };
    },
    computed: {
        // 基本计算属性
        fullName() {
            return this.firstName + ' ' + this.lastName;
        },
        
        // 带 getter 和 setter
        reversedMessage: {
            get() {
                return this.message.split('').reverse().join('');
            },
            set(newValue) {
                this.message = newValue.split('').reverse().join('');
            }
        },
        
        // 依赖其他计算属性
        computedFullName() {
            return this.fullName.toUpperCase();
        },
        
        // 计算属性缓存特性
        totalPrice() {
            console.log('计算总价');
            return this.items.reduce((sum, item) => sum + item.price, 0);
        },
        
        // 计算属性 vs 方法
        expensiveItems() {
            return this.items.filter(item => item.price > 3);
        }
    },
    methods: {
        getTotalPrice() {
            console.log('调用方法');
            return this.items.reduce((sum, item) => sum + item.price, 0);
        }
    }
};
// Composition API
import { computed, ref, reactive } from 'vue';

export default {
    setup() {
        const firstName = ref('');
        const lastName = ref('');
        
        // 计算属性
        const fullName = computed(() => {
            return `${firstName.value} ${lastName.value}`;
        });
        
        // 可写计算属性
        const fullName2 = computed({
            get() {
                return `${firstName.value} ${lastName.value}`;
            },
            set(newValue) {
                [firstName.value, lastName.value] = newValue.split(' ');
            }
        });
        
        return {
            firstName,
            lastName,
            fullName,
            fullName2
        };
    }
};

2.4 侦听器

// Options API
export default {
    data() {
        return {
            question: '',
            answer: '请提出问题',
            user: {
                name: '张三',
                age: 25
            },
            deepObject: {
                nested: {
                    value: 1
                }
            }
        };
    },
    watch: {
        // 基本侦听
        question(newQuestion, oldQuestion) {
            if (newQuestion.includes('?')) {
                this.getAnswer();
            }
        },
        
        // 立即执行
        question: {
            handler(newQuestion, oldQuestion) {
                console.log('问题变化:', newQuestion);
            },
            immediate: true
        },
        
        // 深度侦听
        user: {
            handler(newVal, oldVal) {
                console.log('用户信息变化');
            },
            deep: true
        },
        
        // 侦听嵌套属性
        'deepObject.nested.value': function(newVal, oldVal) {
            console.log('嵌套值变化:', newVal);
        },
        
        // 侦听计算属性
        fullName(newVal, oldVal) {
            console.log('全名变化:', newVal);
        }
    },
    methods: {
        async getAnswer() {
            this.answer = '思考中...';
            try {
                const res = await fetch('https://yesno.wtf/api');
                const data = await res.json();
                this.answer = data.answer;
            } catch (error) {
                this.answer = '错误: ' + error.message;
            }
        }
    }
};
// Composition API
import { watch, watchEffect, ref, reactive } from 'vue';

export default {
    setup() {
        const question = ref('');
        const answer = ref('请提出问题');
        const user = reactive({
            name: '张三',
            age: 25
        });
        
        // watch 函数
        watch(question, async (newQuestion, oldQuestion) => {
            if (newQuestion.includes('?')) {
                answer.value = '思考中...';
                try {
                    const res = await fetch('https://yesno.wtf/api');
                    const data = await res.json();
                    answer.value = data.answer;
                } catch (error) {
                    answer.value = '错误: ' + error.message;
                }
            }
        });
        
        // 侦听多个源
        watch(
            [question, answer],
            ([newQuestion, newAnswer], [oldQuestion, oldAnswer]) => {
                console.log('问题:', newQuestion, '答案:', newAnswer);
            }
        );
        
        // 侦听响应式对象
        watch(
            () => user.name,
            (newName, oldName) => {
                console.log('用户名变化:', newName);
            }
        );
        
        // 深度侦听
        watch(
            user,
            (newVal, oldVal) => {
                console.log('用户信息变化');
            },
            { deep: true }
        );
        
        // watchEffect - 自动追踪依赖
        watchEffect(() => {
            console.log('问题变化:', question.value);
            // 这里会自动追踪 question.value 的依赖
        });
        
        return {
            question,
            answer,
            user
        };
    }
};

2.5 样式与 Class 绑定

<template>
    <!-- Class 绑定 -->
    <!-- 1. 对象语法 -->
    <div :class="{ active: isActive, 'text-danger': hasError }"></div>
    
    <!-- 2. 数组语法 -->
    <div :class="[activeClass, errorClass]"></div>
    
    <!-- 3. 数组和对象混合 -->
    <div :class="[{ active: isActive }, errorClass]"></div>
    
    <!-- 4. 在组件上使用 -->
    <my-component :class="{ active: isActive }"></my-component>
    
    <!-- Style 绑定 -->
    <!-- 1. 对象语法 -->
    <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
    
    <!-- 2. 数组语法(多个样式对象) -->
    <div :style="[baseStyles, overridingStyles]"></div>
    
    <!-- 3. 自动前缀 -->
    <div :style="{ display: 'flex' }"></div>
    
    <!-- 4. 多重值 -->
    <div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
    
    <!-- 5. CSS 变量 -->
    <div :style="{
        '--primary-color': primaryColor,
        '--secondary-color': secondaryColor
    }">
        使用 CSS 变量
    </div>
</template>
export default {
    data() {
        return {
            isActive: true,
            hasError: false,
            activeClass: 'active',
            errorClass: 'text-danger',
            activeColor: 'red',
            fontSize: 30,
            primaryColor: '#42b983',
            secondaryColor: '#35495e',
            baseStyles: {
                color: 'red',
                fontSize: '20px'
            },
            overridingStyles: {
                fontWeight: 'bold'
            }
        };
    }
};

2.6 条件渲染

<template>
    <!-- v-if, v-else-if, v-else -->
    <div v-if="type === 'A'">A</div>
    <div v-else-if="type === 'B'">B</div>
    <div v-else>C</div>
    
    <!-- 在 template 上使用 v-if -->
    <template v-if="show">
        <h1>标题</h1>
        <p>段落</p>
    </template>
    
    <!-- v-show -->
    <h1 v-show="ok">你好!</h1>
    
    <!-- v-if vs v-show -->
    <div v-if="Math.random() > 0.5">v-if: 条件渲染(切换开销高)</div>
    <div v-show="Math.random() > 0.5">v-show: 条件显示(初始渲染开销高)</div>
    
    <!-- 通过 key 管理可复用元素 -->
    <template v-if="loginType === 'username'">
        <label>用户名</label>
        <input placeholder="输入用户名" key="username-input">
    </template>
    <template v-else>
        <label>邮箱</label>
        <input placeholder="输入邮箱" key="email-input">
    </template>
</template>

2.7 列表渲染

<template>
    <!-- 数组渲染 -->
    <ul>
        <li v-for="(item, index) in items" :key="item.id">
             {{  index }} :  {{  item.message }} 
        </li>
    </ul>
    
    <!-- 对象渲染 -->
    <ul>
        <li v-for="(value, key, index) in myObject" :key="key">
             {{  index }} .  {{  key }} :  {{  value }} 
        </li>
    </ul>
    
    <!-- 范围渲染 -->
    <div>
        <span v-for="n in 10" :key="n"> {{  n }}  </span>
    </div>
    
    <!-- 在 template 上使用 v-for -->
    <ul>
        <template v-for="item in items" :key="item.id">
            <li> {{  item.message }} </li>
            <li class="divider" role="presentation"></li>
        </template>
    </ul>
    
    <!-- 组件上使用 v-for -->
    <my-component
        v-for="(item, index) in items"
        :key="item.id"
        :item="item"
        :index="index"
    />
    
    <!-- 显示过滤/排序结果 -->
    <ul>
        <!-- 在计算属性中处理 -->
        <li v-for="user in activeUsers" :key="user.id">
             {{  user.name }} 
        </li>
        
        <!-- 在方法中处理 -->
        <li v-for="n in even(numbers)" :key="n"> {{  n }} </li>
        
        <!-- 嵌套 v-for -->
        <template v-for="list in lists" :key="list.id">
            <h3> {{  list.title }} </h3>
            <ul>
                <li v-for="item in list.items" :key="item.id">
                     {{  item.name }} 
                </li>
            </ul>
        </template>
    </ul>
</template>
export default {
    data() {
        return {
            items: [
                { id: 1, message: 'Foo' },
                { id: 2, message: 'Bar' }
            ],
            myObject: {
                title: '标题',
                author: '作者',
                publishedAt: '2016-04-10'
            },
            numbers: [1, 2, 3, 4, 5],
            lists: [
                {
                    id: 1,
                    title: '购物清单',
                    items: [
                        { id: 1, name: '苹果' },
                        { id: 2, name: '香蕉' }
                    ]
                }
            ],
            users: [
                { id: 1, name: '张三', active: true },
                { id: 2, name: '李四', active: false }
            ]
        };
    },
    computed: {
        activeUsers() {
            return this.users.filter(user => user.active);
        }
    },
    methods: {
        even(numbers) {
            return numbers.filter(number => number % 2 === 0);
        }
    }
};

2.8 事件处理

<template>
    <!-- 内联处理器 -->
    <button @click="count++">增加  {{  count }} </button>
    
    <!-- 方法处理器 -->
    <button @click="greet">问候</button>
    
    <!-- 内联处理器中访问原生 DOM 事件 -->
    <button @click="warn('Form cannot be submitted yet.', $event)">
        提交
    </button>
    
    <!-- 多事件处理器 -->
    <button @click="one($event), two($event)">
        点击
    </button>
    
    <!-- 事件修饰符 -->
    <!-- 阻止默认行为 -->
    <form @submit.prevent="onSubmit"></form>
    
    <!-- 阻止事件冒泡 -->
    <div @click.stop="doThis"></div>
    
    <!-- 阻止默认行为,不阻止冒泡 -->
    <form @submit.prevent></form>
    
    <!-- 串联修饰符 -->
    <a @click.stop.prevent="doThat"></a>
    
    <!-- 添加事件监听器时使用事件捕获模式 -->
    <div @click.capture="doThis"></div>
    
    <!-- 只当事件在该元素本身触发时触发回调 -->
    <div @click.self="doThat"></div>
    
    <!-- 点击事件最多触发一次 -->
    <button @click.once="doThis"></button>
    
    <!-- 滚动事件的默认行为会立即触发,包含 `event.preventDefault()` 的情况 -->
    <div @scroll.passive="onScroll"></div>
    
    <!-- 按键修饰符 -->
    <input @keyup.enter="submit">
    <input @keyup.13="submit"> <!-- 键码 -->
    
    <!-- 按键别名 -->
    <!-- .enter .tab .delete .esc .space .up .down .left .right -->
    <input @keyup.page-down="onPageDown">
    
    <!-- 系统修饰键 -->
    <!-- .ctrl .alt .shift .meta -->
    <div @click.ctrl="doSomething">Ctrl + 点击</div>
    
    <!-- 精确修饰符 -->
    <button @click.ctrl.exact="onCtrlClick">有且只有 Ctrl 被按下</button>
    <button @click.exact="onClick">没有任何系统修饰符被按下</button>
    
    <!-- 鼠标按钮修饰符 -->
    <!-- .left .right .middle -->
    <button @click.right="onRightClick">右键点击</button>
</template>
export default {
    data() {
        return {
            count: 0,
            name: 'Vue.js'
        };
    },
    methods: {
        greet(event) {
            alert('Hello ' + this.name + '!');
            if (event) {
                alert(event.target.tagName);
            }
        },
        warn(message, event) {
            if (event) {
                event.preventDefault();
            }
            alert(message);
        },
        doThis() {
            console.log('doThis');
        },
        doThat() {
            console.log('doThat');
        },
        onSubmit() {
            console.log('表单提交');
        },
        submit() {
            console.log('提交');
        },
        onPageDown() {
            console.log('PageDown');
        },
        onCtrlClick() {
            console.log('Ctrl + Click');
        },
        onClick() {
            console.log('Click');
        },
        onRightClick() {
            console.log('右键点击');
        },
        onScroll() {
            console.log('滚动');
        },
        one(event) {
            console.log('第一个处理器');
        },
        two(event) {
            console.log('第二个处理器');
        }
    }
};

2.9 表单输入绑定

<template>
    <!-- 文本 -->
    <input v-model="message" placeholder="编辑我">
    <p>消息是:  {{  message }} </p>
    
    <!-- 多行文本 -->
    <textarea v-model="message" placeholder="多行输入"></textarea>
    
    <!-- 复选框 -->
    <!-- 单个复选框,绑定布尔值 -->
    <input type="checkbox" id="checkbox" v-model="checked">
    <label for="checkbox"> {{  checked }} </label>
    
    <!-- 多个复选框,绑定到同一个数组 -->
    <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
    <label for="jack">Jack</label>
    <input type="checkbox" id="john" value="John" v-model="checkedNames">
    <label for="john">John</label>
    <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
    <label for="mike">Mike</label>
    <span>选中的名字:  {{  checkedNames }} </span>
    
    <!-- 单选按钮 -->
    <input type="radio" id="one" value="One" v-model="picked">
    <label for="one">One</label>
    <input type="radio" id="two" value="Two" v-model="picked">
    <label for="two">Two</label>
    <span>选中的是:  {{  picked }} </span>
    
    <!-- 选择框 -->
    <!-- 单选 -->
    <select v-model="selected">
        <option disabled value="">请选择</option>
        <option>A</option>
        <option>B</option>
        <option>C</option>
    </select>
    <span>选中的:  {{  selected }} </span>
    
    <!-- 多选(绑定到数组) -->
    <select v-model="multiSelected" multiple style="width: 50px;">
        <option>A</option>
        <option>B</option>
        <option>C</option>
    </select>
    <span>选中的:  {{  multiSelected }} </span>
    
    <!-- 动态选项 -->
    <select v-model="selected">
        <option v-for="option in options" :key="option.value" :value="option.value">
             {{  option.text }} 
        </option>
    </select>
    <span>选中的:  {{  selected }} </span>
    
    <!-- 值绑定 -->
    <!-- 复选框 -->
    <input
        type="checkbox"
        v-model="toggle"
        true-value="yes"
        false-value="no"
    >
    <p> {{  toggle }} </p>
    
    <!-- 单选按钮 -->
    <input type="radio" v-model="pick" :value="a">
    <p> {{  pick }} </p>
    
    <!-- 选择框选项 -->
    <select v-model="selected">
        <!-- 内联对象字面量 -->
        <option :value="{ number: 123 }">123</option>
    </select>
    <p> {{  selected.number }} </p>
    
    <!-- 修饰符 -->
    <!-- .lazy - 在 change 事件后同步 -->
    <input v-model.lazy="msg">
    <p> {{  msg }} </p>
    
    <!-- .number - 输入值转为数字 -->
    <input v-model.number="age" type="number">
    <p> {{  typeof age }} </p>
    
    <!-- .trim - 自动去除首尾空白字符 -->
    <input v-model.trim="trimmed">
    <p>" {{  trimmed }} "</p>
</template>
export default {
    data() {
        return {
            message: '',
            checked: false,
            checkedNames: [],
            picked: '',
            selected: '',
            multiSelected: [],
            options: [
                { text: 'One', value: 'A' },
                { text: 'Two', value: 'B' },
                { text: 'Three', value: 'C' }
            ],
            toggle: '',
            pick: '',
            a: 'a',
            msg: '',
            age: 0,
            trimmed: ''
        };
    }
};

三、组件基础

3.1 组件注册

// 全局组件注册
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 1. 组件选项对象
app.component('my-component', {
    template: '<div>自定义组件!</div>'
});

// 2. 单文件组件
import MyComponent from './MyComponent.vue';
app.component('MyComponent', MyComponent);

// 3. 异步组件
app.component(
    'AsyncComponent',
    () => import('./AsyncComponent.vue')
);

// 局部组件注册
export default {
    components: {
        // 1. 组件选项对象
        'my-component': {
            template: '<div>局部组件</div>'
        },
        // 2. 导入的组件
        MyComponent: MyComponent,
        // 3. 简写
        MyComponent
    }
};

3.2 Props

// 子组件
export default {
    // 1. 字符串数组形式
    props: ['title', 'likes', 'isPublished', 'commentIds', 'author'],
    
    // 2. 对象形式
    props: {
        title: String,
        likes: Number,
        isPublished: Boolean,
        commentIds: Array,
        author: Object,
        callback: Function,
        contactsPromise: Promise
    },
    
    // 3. 详细配置
    props: {
        // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
        propA: Number,
        
        // 多个可能的类型
        propB: [String, Number],
        
        // 必填的字符串
        propC: {
            type: String,
            required: true
        },
        
        // 带有默认值的数字
        propD: {
            type: Number,
            default: 100
        },
        
        // 带有默认值的对象
        propE: {
            type: Object,
            // 对象或数组默认值必须从一个工厂函数返回
            default() {
                return { message: 'hello' };
            }
        },
        
        // 自定义验证函数
        propF: {
            validator(value) {
                // 这个值必须匹配下列字符串中的一个
                return ['success', 'warning', 'danger'].includes(value);
            }
        },
        
        // 自定义类型构造函数
        propG: {
            type: Person,  // 自定义构造函数
            default: () => new Person('John')
        },
        
        // 自定义类型检查
        propH: {
            type: [String, Number, Boolean],
            required: true,
            validator(value) {
                return value !== '';
            }
        }
    },
    
    // 4. Composition API
    setup(props, context) {
        // props 是响应式的
        console.log(props.title);
        
        // 解构 props
        const { title } = toRefs(props);
        
        return {
            title
        };
    }
};
<!-- 父组件使用 -->
<template>
    <!-- 传递静态值 -->
    <blog-post title="My journey with Vue"></blog-post>
    
    <!-- 动态绑定 -->
    <blog-post :title="post.title"></blog-post>
    
    <!-- 传入一个数字 -->
    <blog-post :likes="42"></blog-post>
    
    <!-- 传入一个布尔值 -->
    <blog-post :is-published="false"></blog-post>
    
    <!-- 传入一个数组 -->
    <blog-post :comment-ids="[234, 266, 273]"></blog-post>
    
    <!-- 传入一个对象 -->
    <blog-post :author="{ name: 'Veronica', company: 'Veridian Dynamics' }"></blog-post>
    
    <!-- 传入一个对象的所有 property -->
    <blog-post v-bind="post"></blog-post>
    <!-- 等价于 -->
    <blog-post
        :id="post.id"
        :title="post.title"
    ></blog-post>
    
    <!-- 单向数据流 -->
    <!-- 在子组件中不要直接修改 props -->
</template>

3.3 自定义事件

// 子组件
export default {
    emits: [
        // 数组语法
        'enlarge-text',
        'update:title'
    ],
    
    emits: {
        // 对象语法
        'enlarge-text': null,  // 没有验证
        
        'update:title': (title) => {
            if (title && typeof title === 'string') {
                return true;
            } else {
                console.warn('Invalid title!');
                return false;
            }
        }
    },
    
    methods: {
        submitForm() {
            // 触发自定义事件
            this.$emit('enlarge-text');
            this.$emit('update:title', '新的标题');
            
            // 传递多个参数
            this.$emit('submit', formData, extraData);
            
            // Composition API
            const { emit } = getCurrentInstance();
            emit('custom-event', payload);
        }
    }
};
<!-- 父组件 -->
<template>
    <!-- 监听事件 -->
    <blog-post @enlarge-text="postFontSize += 0.1"></blog-post>
    
    <!-- 事件处理函数接收参数 -->
    <blog-post @update:title="handleUpdate"></blog-post>
    
    <!-- 内联处理器 -->
    <blog-post @update:title="title = $event"></blog-post>
    
    <!-- 多个事件处理器 -->
    <blog-post
        @update:title="handleUpdate"
        @delete="handleDelete"
    ></blog-post>
    
    <!-- 事件修饰符 -->
    <my-component @click.native="handleNativeClick"></my-component>
    
    <!-- v-model 自定义 -->
    <custom-input v-model="searchText"></custom-input>
    <!-- 等价于 -->
    <custom-input
        :model-value="searchText"
        @update:model-value="newValue => searchText = newValue"
    ></custom-input>
</template>

3.4 插槽

<!-- 子组件 -->
<template>
    <!-- 默认插槽 -->
    <div class="container">
        <header>
            <slot name="header"></slot>
        </header>
        <main>
            <slot></slot>  <!-- 默认插槽 -->
        </main>
        <footer>
            <slot name="footer"></slot>
        </footer>
    </div>
    
    <!-- 作用域插槽 -->
    <ul>
        <li v-for="(item, index) in items" :key="item.id">
            <slot :item="item" :index="index"></slot>
        </li>
    </ul>
    
    <!-- 具名作用域插槽 -->
    <slot name="item" v-bind="{ value }"></slot>
    
    <!-- 动态插槽名 -->
    <template v-for="(_, slot) in $slots" :key="slot">
        <slot :name="slot" />
    </template>
</template>
<!-- 父组件 -->
<template>
    <!-- 默认插槽 -->
    <base-layout>
        <template v-slot:header>
            <h1>标题</h1>
        </template>
        
        <p>主要内容</p>
        
        <template v-slot:footer>
            <p>页脚</p>
        </template>
    </base-layout>
    
    <!-- 缩写 -->
    <base-layout>
        <template #header>
            <h1>标题</h1>
        </template>
    </base-layout>
    
    <!-- 作用域插槽 -->
    <todo-list>
        <template v-slot:default="slotProps">
            <span v-if="slotProps.item.isComplete"></span>
             {{  slotProps.item.text }} 
        </template>
    </todo-list>
    
    <!-- 解构插槽 Prop -->
    <todo-list>
        <template v-slot="{ item }">
            <span v-if="item.isComplete"></span>
             {{  item.text }} 
        </template>
    </todo-list>
    
    <!-- 具名作用域插槽 -->
    <scoped-slot-demo>
        <template #item="{ value }">
            <span> {{  value }} </span>
        </template>
    </scoped-slot-demo>
    
    <!-- 动态插槽名 -->
    <base-layout>
        <template #[dynamicSlotName]>
            ...
        </template>
    </base-layout>
</template>

3.5 动态组件

<template>
    <!-- 动态组件 -->
    <component :is="currentComponent"></component>
    
    <!-- 保持组件状态 -->
    <keep-alive>
        <component :is="currentComponent"></component>
    </keep-alive>
    
    <!-- 包含/排除特定组件 -->
    <keep-alive :include="['Post', 'Archive']" :exclude="['Home']" :max="10">
        <component :is="currentComponent"></component>
    </keep-alive>
    
    <!-- 组件切换过渡 -->
    <transition name="fade" mode="out-in">
        <component :is="currentComponent"></component>
    </transition>
    
    <!-- 组件名可以是 -->
    <!-- 1. 注册的组件名 -->
    <component :is="'my-component'"></component>
    
    <!-- 2. 导入的组件对象 -->
    <component :is="importedComponent"></component>
    
    <!-- 3. 组件选项对象 -->
    <component :is="{ template: '<div>动态组件</div>' }"></component>
</template>
import { shallowRef } from 'vue';
import Home from './Home.vue';
import About from './About.vue';
import Contact from './Contact.vue';

export default {
    components: {
        Home,
        About,
        Contact
    },
    data() {
        return {
            currentComponent: shallowRef(Home),
            tabs: [
                { name: 'Home', component: Home },
                { name: 'About', component: About },
                { name: 'Contact', component: Contact }
            ],
            currentTab: 0
        };
    },
    methods: {
        changeComponent(component) {
            this.currentComponent = component;
        }
    }
};

3.6 异步组件

// 全局注册异步组件
import { defineAsyncComponent } from 'vue';

// 1. 基本用法
const AsyncComp = defineAsyncComponent(() =>
    import('./components/MyComponent.vue')
);

// 2. 带选项的工厂函数
const AsyncCompWithOptions = defineAsyncComponent({
    // 加载函数
    loader: () => import('./Foo.vue'),
    
    // 加载异步组件时使用的组件
    loadingComponent: LoadingComponent,
    
    // 展示加载组件前的延迟时间,默认为 200ms
    delay: 200,
    
    // 加载失败后展示的组件
    errorComponent: ErrorComponent,
    
    // 如果提供了 timeout,并且加载组件的时间超过了设定值,也会展示报错组件
    timeout: 3000,
    
    // 定义组件是否可挂起
    suspensible: false,
    
    // 错误处理函数
    onError(error, retry, fail, attempts) {
        if (error.message.match(/fetch/) && attempts <= 3) {
            // 重试获取错误
            retry();
        } else {
            // 注意,retry/fail 类似于 promise 的 resolve/reject
            fail();
        }
    }
});

// 3. 局部注册异步组件
export default {
    components: {
        AsyncComponent: defineAsyncComponent(() =>
            import('./components/AsyncComponent.vue')
        )
    }
};

3.7 依赖注入

// 父组件提供
import { provide, reactive, readonly } from 'vue';

export default {
    // Options API
    provide: {
        // 提供静态值
        message: 'hello',
        
        // 提供响应式数据
        user: {
            name: '张三',
            age: 25
        }
    },
    
    // 提供函数
    provide() {
        return {
            // 使注入成为只读
            theme: readonly(this.theme),
            
            // 提供方法
            updateTheme: this.updateTheme
        };
    },
    
    // Composition API
    setup() {
        const theme = reactive({
            primary: '#1890ff',
            secondary: '#f0f2f5'
        });
        
        // 提供响应式数据
        provide('theme', theme);
        
        // 提供方法
        provide('updateTheme', (newTheme) => {
            Object.assign(theme, newTheme);
        });
        
        // 提供 Symbol 作为 key
        const ThemeSymbol = Symbol('theme');
        provide(ThemeSymbol, theme);
        
        return {
            theme
        };
    }
};
// 子组件注入
import { inject } from 'vue';

export default {
    // Options API
    inject: [
        'message',  // 数组语法
        'user',
        'theme'
    ],
    
    inject: {
        // 对象语法
        localMessage: {
            from: 'message',  // 从哪个 property 注入
            default: '默认消息'  // 默认值
        },
        
        // 简写
        user: {
            default: () => ({ name: '游客' })
        },
        
        // 工厂函数
        theme: {
            default: () => ({
                primary: '#1890ff',
                secondary: '#f0f2f5'
            })
        }
    },
    
    // Composition API
    setup() {
        // 注入值
        const message = inject('message');
        
        // 带默认值
        const theme = inject('theme', {
            primary: '#1890ff',
            secondary: '#f0f2f5'
        });
        
        // 响应式注入
        const user = inject('user', {});
        
        // 注入方法
        const updateTheme = inject('updateTheme');
        
        // Symbol key
        const ThemeSymbol = Symbol('theme');
        const theme2 = inject(ThemeSymbol);
        
        return {
            message,
            theme,
            user,
            updateTheme
        };
    }
};

四、深入组件

4.1 组件通信

// 1. Props / Events (父子组件)
// 父 -> 子: Props
// 子 -> 父: Events

// 2. v-model (双向绑定)
// 自定义组件实现 v-model
export default {
    props: ['modelValue'],
    emits: ['update:modelValue'],
    template: `
        <input
            :value="modelValue"
            @input="$emit('update:modelValue', $event.target.value)"
        >
    `
};

// 3. .sync 修饰符 (Vue 2)
// 在 Vue 3 中,使用 v-model:propName 替代

// 4. $parent / $children (不推荐)
// 访问父组件实例
this.$parent.someMethod();
// 访问子组件实例
this.$children[0].someMethod();

// 5. $refs
// 父组件
<template>
    <child-component ref="childRef"></child-component>
</template>

<script>
export default {
    mounted() {
        // 访问子组件实例
        this.$refs.childRef.someMethod();
        // 访问 DOM 元素
        this.$refs.input.focus();
    }
};
</script>

// 6. Provide / Inject (跨级组件)
// 见 3.7 节

// 7. Event Bus (不推荐,使用 Vuex/Pinia 替代)
// 创建事件总线
// eventBus.js
import mitt from 'mitt';
export const emitter = mitt();

// 发送事件
emitter.emit('event-name', data);

// 接收事件
emitter.on('event-name', (data) => {
    console.log(data);
});

// 8. Vuex / Pinia (状态管理)
// 见第 6 章

// 9. 全局状态
// 创建全局状态
// globalState.js
import { reactive } from 'vue';
export const globalState = reactive({
    count: 0,
    increment() {
        this.count++;
    }
});

// 在组件中使用
import { globalState } from './globalState';
console.log(globalState.count);
globalState.increment();

4.2 组件生命周期

// Options API
export default {
    data() {
        return {
            message: 'Hello Vue!'
        };
    },
    
    // 生命周期钩子
    beforeCreate() {
        // 实例初始化之后,data observer 和 event/watcher 事件配置之前
        console.log('beforeCreate');
    },
    
    created() {
        // 实例创建完成后,data observer 和 event/watcher 已配置
        console.log('created');
        // 可以访问 data、computed、methods
        console.log(this.message);
    },
    
    beforeMount() {
        // 挂载开始之前,render 函数首次被调用
        console.log('beforeMount');
    },
    
    mounted() {
        // 实例被挂载后,DOM 已更新
        console.log('mounted');
        // 可以访问 $el
        console.log(this.$el);
    },
    
    beforeUpdate() {
        // 数据更新时,虚拟 DOM 重新渲染和打补丁之前
        console.log('beforeUpdate');
    },
    
    updated() {
        // 数据更新导致虚拟 DOM 重新渲染和打补丁之后
        console.log('updated');
    },
    
    beforeUnmount() {
        // 实例销毁之前
        console.log('beforeUnmount');
    },
    
    unmounted() {
        // 实例销毁后
        console.log('unmounted');
    },
    
    // 其他钩子
    activated() {
        // 被 keep-alive 缓存的组件激活时调用
        console.log('activated');
    },
    
    deactivated() {
        // 被 keep-alive 缓存的组件失活时调用
        console.log('deactivated');
    },
    
    errorCaptured(err, vm, info) {
        // 捕获子孙组件的错误
        console.log('errorCaptured', err, info);
        return false; // 阻止错误继续向上传播
    },
    
    renderTracked(e) {
        // 跟踪虚拟 DOM 重新渲染时调用
        console.log('renderTracked', e);
    },
    
    renderTriggered(e) {
        // 虚拟 DOM 重新渲染被触发时调用
        console.log('renderTriggered', e);
    }
};
// Composition API
import { 
    onBeforeMount, 
    onMounted, 
    onBeforeUpdate, 
    onUpdated,
    onBeforeUnmount,
    onUnmounted,
    onActivated,
    onDeactivated,
    onErrorCaptured,
    onRenderTracked,
    onRenderTriggered
} from 'vue';

export default {
    setup() {
        // 组件挂载阶段
        onBeforeMount(() => {
            console.log('onBeforeMount');
        });
        
        onMounted(() => {
            console.log('onMounted');
        });
        
        // 组件更新阶段
        onBeforeUpdate(() => {
            console.log('onBeforeUpdate');
        });
        
        onUpdated(() => {
            console.log('onUpdated');
        });
        
        // 组件卸载阶段
        onBeforeUnmount(() => {
            console.log('onBeforeUnmount');
        });
        
        onUnmounted(() => {
            console.log('onUnmounted');
        });
        
        // 组件激活/停用
        onActivated(() => {
            console.log('onActivated');
        });
        
        onDeactivated(() => {
            console.log('onDeactivated');
        });
        
        // 错误捕获
        onErrorCaptured((err, instance, info) => {
            console.log('onErrorCaptured', err, info);
            return false;
        });
        
        // 调试钩子
        onRenderTracked((e) => {
            console.log('onRenderTracked', e);
        });
        
        onRenderTriggered((e) => {
            console.log('onRenderTriggered', e);
        });
        
        return {};
    }
};

4.3 模板引用

<template>
    <!-- 模板引用 -->
    <input ref="input" />
    
    <!-- 在 v-for 中使用 -->
    <div v-for="item in list" :ref="setItemRef">
         {{  item }} 
    </div>
    
    <!-- 组件引用 -->
    <child-component ref="child" />
    
    <!-- 函数引用 -->
    <child-component :ref="(el) => { childRef = el }" />
    
    <!-- Composition API -->
    <div ref="divRef">内容</div>
</template>
import { ref, onMounted, nextTick } from 'vue';

export default {
    // Options API
    mounted() {
        // 访问 DOM 元素
        this.$refs.input.focus();
        
        // 访问组件实例
        this.$refs.child.someMethod();
        
        // v-for 中的引用
        console.log(this.itemRefs);
    },
    
    data() {
        return {
            itemRefs: []
        };
    },
    
    methods: {
        setItemRef(el) {
            if (el) {
                this.itemRefs.push(el);
            }
        }
    },
    
    // Composition API
    setup() {
        const input = ref(null);
        const child = ref(null);
        const divRef = ref(null);
        
        onMounted(() => {
            // 访问 DOM 元素
            input.value.focus();
            
            // 访问组件实例
            child.value.someMethod();
            
            // 等待下一个 DOM 更新周期
            nextTick(() => {
                console.log('DOM 已更新');
            });
        });
        
        return {
            input,
            child,
            divRef
        };
    }
};

4.4 过渡与动画

<template>
    <!-- 单元素/组件过渡 -->
    <transition name="fade">
        <p v-if="show">hello</p>
    </transition>
    
    <!-- 多个元素的过渡 -->
    <transition name="fade" mode="out-in">
        <button v-if="isEditing" key="save" @click="isEditing = false">
            保存
        </button>
        <button v-else key="edit" @click="isEditing = true">
            编辑
        </button>
    </transition>
    
    <!-- 多个组件过渡 -->
    <transition name="component-fade" mode="out-in">
        <component :is="currentComponent"></component>
    </transition>
    
    <!-- 列表过渡 -->
    <transition-group name="list" tag="ul">
        <li v-for="item in items" :key="item.id">
             {{  item.text }} 
        </li>
    </transition-group>
    
    <!-- 动态过渡 -->
    <transition :name="transitionName">
        <p v-if="show">hello</p>
    </transition>
    
    <!-- JavaScript 钩子 -->
    <transition
        @before-enter="beforeEnter"
        @enter="enter"
        @after-enter="afterEnter"
        @enter-cancelled="enterCancelled"
        @before-leave="beforeLeave"
        @leave="leave"
        @after-leave="afterLeave"
        @leave-cancelled="leaveCancelled"
    >
        <p v-if="show">hello</p>
    </transition>
</template>
export default {
    data() {
        return {
            show: true,
            isEditing: false,
            currentComponent: 'component-a',
            items: [
                { id: 1, text: 'Item 1' },
                { id: 2, text: 'Item 2' }
            ],
            transitionName: 'fade'
        };
    },
    
    methods: {
        // JavaScript 钩子
        beforeEnter(el) {
            el.style.opacity = 0;
            el.style.transform = 'translateY(30px)';
        },
        
        enter(el, done) {
            gsap.to(el, {
                opacity: 1,
                y: 0,
                duration: 0.5,
                onComplete: done
            });
        },
        
        afterEnter(el) {
            console.log('进入完成');
        },
        
        enterCancelled(el) {
            console.log('进入取消');
        },
        
        beforeLeave(el) {
            console.log('离开之前');
        },
        
        leave(el, done) {
            gsap.to(el, {
                opacity: 0,
                y: 30,
                duration: 0.5,
                onComplete: done
            });
        },
        
        afterLeave(el) {
            console.log('离开完成');
        },
        
        leaveCancelled(el) {
            console.log('离开取消');
        }
    }
};
/* CSS 过渡类名 */
/* 进入/离开 */
.fade-enter-from {
    opacity: 0;
}
.fade-enter-to {
    opacity: 1;
}
.fade-enter-active {
    transition: opacity 0.5s ease;
}
.fade-leave-from {
    opacity: 1;
}
.fade-leave-to {
    opacity: 0;
}
.fade-leave-active {
    transition: opacity 0.5s ease;
}

/* 列表过渡 */
.list-enter-from,
.list-leave-to {
    opacity: 0;
    transform: translateY(30px);
}
.list-enter-to,
.list-leave-from {
    opacity: 1;
    transform: translateY(0);
}
.list-enter-active,
.list-leave-active {
    transition: all 0.5s ease;
}
.list-move {
    transition: transform 0.5s ease;
}
.list-leave-active {
    position: absolute;
}

4.5 混入

// mixin.js
export const myMixin = {
    data() {
        return {
            mixinData: '来自混入的数据'
        };
    },
    
    created() {
        console.log('混入的 created 钩子');
    },
    
    methods: {
        mixinMethod() {
            console.log('混入的方法');
        }
    }
};
// 组件中使用
import { myMixin } from './mixin.js';

// 局部混入
export default {
    mixins: [myMixin],
    
    data() {
        return {
            // 与混入对象的数据合并
            mixinData: '组件的数据',  // 组件数据优先
            componentData: '组件独有数据'
        };
    },
    
    created() {
        console.log('组件的 created 钩子');
        // 都会调用,混入的先调用
    },
    
    methods: {
        // 与混入对象的方法合并
        mixinMethod() {
            console.log('组件的方法');  // 组件方法优先
        },
        
        componentMethod() {
            console.log('组件独有方法');
        }
    }
};
// 全局混入
import { createApp } from 'vue';

const app = createApp(App);

app.mixin({
    created() {
        console.log('全局混入的 created 钩子');
    }
});

4.6 自定义指令

// 全局自定义指令
import { createApp } from 'vue';

const app = createApp(App);

// 1. 函数简写
app.directive('focus', (el, binding) => {
    el.focus();
});

// 2. 完整对象
app.directive('pin', {
    mounted(el, binding) {
        el.style.position = 'fixed';
        
        const s = binding.arg || 'top';
        el.style[s] = binding.value + 'px';
    },
    
    updated(el, binding) {
        const s = binding.arg || 'top';
        el.style[s] = binding.value + 'px';
    },
    
    beforeUnmount(el, binding) {
        // 清理工作
    }
});
// 局部自定义指令
export default {
    directives: {
        // 局部指令
        focus: {
            mounted(el) {
                el.focus();
            }
        },
        
        // 简写
        color(el, binding) {
            el.style.color = binding.value;
        }
    },
    
    setup() {
        // Composition API
        const vMyDirective = {
            mounted(el, binding) {
                // 指令逻辑
            }
        };
        
        return {
            vMyDirective
        };
    }
};
<template>
    <!-- 使用指令 -->
    <input v-focus />
    
    <!-- 带值 -->
    <div v-color="'red'">红色文字</div>
    
    <!-- 动态参数 -->
    <div v-pin:top="200">固定在顶部</div>
    
    <!-- 修饰符 -->
    <div v-demo.modifier="value">修饰符示例</div>
    
    <!-- 对象字面量 -->
    <div v-demo="{ color: 'white', text: 'hello!' }"></div>
</template>

4.7 渲染函数

// Options API
export default {
    data() {
        return {
            message: 'Hello Vue!'
        };
    },
    
    render() {
        // 使用 h() 函数创建虚拟 DOM
        return h(
            'div',  // 标签名
            { id: 'app' },  // 属性/Props
            [
                h('h1', null, this.message),
                h('button', {
                    onClick: () => {
                        this.message = 'Clicked!';
                    }
                }, '点击我')
            ]
        );
    }
};
// Composition API
import { h, ref } from 'vue';

export default {
    setup() {
        const message = ref('Hello Vue!');
        
        return () => h('div', [
            h('h1', null, message.value),
            h('button', {
                onClick: () => {
                    message.value = 'Clicked!';
                }
            }, '点击我')
        ]);
    }
};
// JSX
import { defineComponent, ref } from 'vue';

export default defineComponent({
    setup() {
        const count = ref(0);
        
        return () => (
            <div>
                <h1>Count: {count.value}</h1>
                <button onClick={() => count.value++}>增加</button>
            </div>
        );
    }
});

4.8 函数式组件

// 函数式组件
export default {
    functional: true,
    props: {
        title: {
            type: String,
            required: true
        }
    },
    
    // 对于函数式组件,没有 this
    // context 参数包含 attrs, slots, emit, props
    render(h, context) {
        return h('div', context.data, [
            h('h2', context.props.title),
            context.children
        ]);
    }
};
// Vue 3 函数式组件
import { h } from 'vue';

const FunctionalComponent = (props, context) => {
    return h('div', context.attrs, [
        h('h2', props.title),
        context.slots.default()
    ]);
};

FunctionalComponent.props = ['title'];

五、组合式 API

5.1 setup 函数

import { ref, reactive, computed, watch, onMounted } from 'vue';

export default {
    // setup 函数
    setup(props, context) {
        // props - 响应式的 props
        console.log(props.title);
        
        // context - 非响应式的对象
        const { 
            attrs,      // 非响应式的 attribute
            slots,      // 插槽
            emit,       // 触发事件
            expose      // 暴露公共属性
        } = context;
        
        // 响应式状态
        const count = ref(0);
        const state = reactive({
            name: '张三',
            age: 25
        });
        
        // 计算属性
        const doubleCount = computed(() => count.value * 2);
        
        // 侦听器
        watch(count, (newVal, oldVal) => {
            console.log(`count 从 ${oldVal} 变为 ${newVal}`);
        });
        
        // 生命周期钩子
        onMounted(() => {
            console.log('组件已挂载');
        });
        
        // 方法
        const increment = () => {
            count.value++;
        };
        
        // 暴露给模板
        return {
            count,
            state,
            doubleCount,
            increment
        };
        
        // 使用 expose
        // expose({
        //     increment
        // });
        // return {};
    }
};

5.2 响应式 API

import { 
    ref, 
    reactive, 
    readonly, 
    toRef, 
    toRefs, 
    toRaw,
    isRef,
    isReactive,
    isReadonly,
    isProxy,
    shallowRef,
    shallowReactive,
    shallowReadonly,
    markRaw
} from 'vue';

export default {
    setup() {
        // ref - 响应式引用
        const count = ref(0);
        const objRef = ref({ name: '张三' });
        
        // reactive - 响应式对象
        const state = reactive({
            name: '张三',
            age: 25,
            address: {
                city: '北京',
                street: '朝阳区'
            }
        });
        
        // readonly - 只读代理
        const readOnlyState = readonly(state);
        // readOnlyState.name = '李四'; // 错误:只读
        
        // toRef - 创建 ref
        const nameRef = toRef(state, 'name');
        
        // toRefs - 解构响应式对象
        const { name, age } = toRefs(state);
        
        // toRaw - 返回原始对象
        const rawState = toRaw(state);
        
        // 类型检查
        console.log(isRef(count));        // true
        console.log(isReactive(state));   // true
        console.log(isReadonly(readOnlyState)); // true
        console.log(isProxy(state));      // true
        
        // shallowRef - 浅层 ref
        const shallowObj = shallowRef({ nested: { value: 1 } });
        shallowObj.value.nested.value = 2; // 不会触发响应
        
        // shallowReactive - 浅层 reactive
        const shallowState = shallowReactive({
            nested: { value: 1 }
        });
        
        // shallowReadonly - 浅层 readonly
        const shallowReadOnly = shallowReadonly({
            nested: { value: 1 }
        });
        
        // markRaw - 标记为不可转为响应式
        const rawObj = markRaw({ id: 1 });
        const state2 = reactive({
            raw: rawObj  // raw 不会被转为响应式
        });
        
        return {
            count,
            state,
            nameRef,
            name,
            age
        };
    }
};

5.3 组合式函数

// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';

export function useMouse() {
    const x = ref(0);
    const y = ref(0);
    
    function update(event) {
        x.value = event.pageX;
        y.value = event.pageY;
    }
    
    onMounted(() => window.addEventListener('mousemove', update));
    onUnmounted(() => window.removeEventListener('mousemove', update));
    
    return { x, y };
}
// useFetch.js
import { ref, isRef, unref, watchEffect } from 'vue';

export function useFetch(url) {
    const data = ref(null);
    const error = ref(null);
    const loading = ref(false);
    
    async function doFetch() {
        data.value = null;
        error.value = null;
        loading.value = true;
        
        try {
            const response = await fetch(unref(url));
            data.value = await response.json();
        } catch (err) {
            error.value = err;
        } finally {
            loading.value = false;
        }
    }
    
    if (isRef(url)) {
        watchEffect(doFetch);
    } else {
        doFetch();
    }
    
    return { data, error, loading, retry: doFetch };
}
// 在组件中使用
import { useMouse, useFetch } from './composables';

export default {
    setup() {
        const { x, y } = useMouse();
        const { data, error, loading } = useFetch('/api/user');
        
        return {
            x,
            y,
            data,
            error,
            loading
        };
    }
};

5.4 依赖注入

// 提供依赖
import { provide, inject, reactive, readonly } from 'vue';

// 创建 Symbol 作为 key
const ThemeSymbol = Symbol('theme');

export function provideTheme() {
    const theme = reactive({
        primary: '#1890ff',
        secondary: '#f0f2f5',
        mode: 'light'
    });
    
    const updateTheme = (newTheme) => {
        Object.assign(theme, newTheme);
    };
    
    provide(ThemeSymbol, {
        theme: readonly(theme),
        updateTheme
    });
    
    return { theme, updateTheme };
}
// 注入依赖
import { inject } from 'vue';
import { ThemeSymbol } from './theme';

export function useTheme() {
    const context = inject(ThemeSymbol);
    
    if (!context) {
        throw new Error('必须在 provideTheme 的组件中使用 useTheme');
    }
    
    return context;
}

5.5 异步组件

// 异步组件
import { defineAsyncComponent } from 'vue';

// 基本用法
const AsyncComp = defineAsyncComponent(() =>
    import('./components/MyComponent.vue')
);

// 带加载状态
const AsyncCompWithLoading = defineAsyncComponent({
    loader: () => import('./Foo.vue'),
    loadingComponent: LoadingComponent,
    delay: 200,
    errorComponent: ErrorComponent,
    timeout: 3000
});

5.6 Suspense

<template>
    <!-- Suspense 组件 -->
    <Suspense>
        <!-- 默认插槽显示异步组件 -->
        <template #default>
            <AsyncComponent />
        </template>
        
        <!-- fallback 插槽显示加载状态 -->
        <template #fallback>
            <div>加载中...</div>
        </template>
    </Suspense>
    
    <!-- 多个异步组件 -->
    <Suspense>
        <template #default>
            <div>
                <AsyncComponent1 />
                <AsyncComponent2 />
            </div>
        </template>
    </Suspense>
    
    <!-- 错误处理 -->
    <Suspense @resolve="onResolve" @pending="onPending" @fallback="onFallback">
        <AsyncComponent />
    </Suspense>
</template>
// 异步组件
export default {
    async setup() {
        // 在 setup 中执行异步操作
        const data = await fetchData();
        
        return {
            data
        };
    }
};

六、状态管理

6.3 Pinia 与 Vuex 对比

特性 Pinia Vuex
创建时间 2019年,Vue 3 时代 2015年,Vue 2 时代
API 设计 组合式 API 风格 选项式 API 风格
TypeScript 支持 一流的 TypeScript 支持 需要类型定义帮助
模块化 自动代码分割,多个 store 模块系统
开发工具 Vue DevTools 支持 Vue DevTools 支持
大小 约 1KB 约 10KB
学习曲线 更简单,概念更少 较复杂,概念较多
推荐使用 Vue 3 项目首选 遗留项目维护

6.4 状态管理最佳实践

// 1. 模块化组织
// store/
//   ├── index.js          // 主 store
//   ├── modules/
//   │   ├── user.js
//   │   ├── cart.js
//   │   └── product.js
//   └── types.js          // TypeScript 类型定义

// 2. 类型安全
// types.ts
export interface User {
    id: number;
    name: string;
    email: string;
    age: number;
}

export interface Product {
    id: number;
    name: string;
    price: number;
    stock: number;
}

// 3. 数据持久化
// 使用插件实现
import { createPinia } from 'pinia';
import piniaPluginPersist from 'pinia-plugin-persist';

const pinia = createPinia();
pinia.use(piniaPluginPersist);

// 4. 服务层分离
// services/userService.js
export class UserService {
    async fetchUsers() {
        const response = await fetch('/api/users');
        return response.json();
    }
    
    async updateUser(user) {
        const response = await fetch(`/api/users/${user.id}`, {
            method: 'PUT',
            body: JSON.stringify(user)
        });
        return response.json();
    }
}

// 5. 状态规范化
// 避免嵌套过深,扁平化数据结构
const normalizedState = {
    users: {
        byId: {
            '1': { id: 1, name: '张三' },
            '2': { id: 2, name: '李四' }
        },
        allIds: [1, 2]
    },
    products: {
        byId: {
            '101': { id: 101, name: '商品1' },
            '102': { id: 102, name: '商品2' }
        },
        allIds: [101, 102]
    }
};

七、路由(Vue Router)

7.1 Vue Router 基础

// 安装和配置
import { createRouter, createWebHistory } from 'vue-router';

// 路由配置
const routes = [
    {
        path: '/',
        name: 'Home',
        component: () => import('@/views/Home.vue'),
        meta: {
            requiresAuth: true,
            title: '首页'
        }
    },
    {
        path: '/about',
        name: 'About',
        component: () => import('@/views/About.vue'),
        meta: {
            title: '关于我们'
        }
    },
    {
        path: '/user/:id',
        name: 'User',
        component: () => import('@/views/User.vue'),
        props: true,  // 将路由参数作为 props 传递
        children: [
            {
                path: 'profile',
                component: () => import('@/views/UserProfile.vue')
            },
            {
                path: 'posts',
                component: () => import('@/views/UserPosts.vue')
            }
        ]
    },
    {
        path: '/product/:id(\\d+)',  // 正则表达式约束
        name: 'Product',
        component: () => import('@/views/Product.vue')
    },
    {
        path: '/:pathMatch(.*)*',  // 404 页面
        name: 'NotFound',
        component: () => import('@/views/NotFound.vue')
    }
];

// 创建路由实例
const router = createRouter({
    history: createWebHistory(),  // HTML5 模式
    // history: createWebHashHistory(),  // Hash 模式
    routes,
    
    // 滚动行为
    scrollBehavior(to, from, savedPosition) {
        if (savedPosition) {
            return savedPosition;
        } else {
            return { top: 0 };
        }
    }
});

// 全局前置守卫
router.beforeEach((to, from, next) => {
    // 修改页面标题
    document.title = to.meta.title || '默认标题';
    
    // 身份验证检查
    if (to.meta.requiresAuth && !isAuthenticated()) {
        next('/login');
    } else {
        next();
    }
});

// 全局后置钩子
router.afterEach((to, from) => {
    // 发送页面浏览统计
    sendToAnalytics(to.fullPath);
});

// 导出
export default router;

7.2 路由组件中使用

// 在组件中使用路由
import { useRouter, useRoute, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router';

export default {
    setup() {
        const router = useRouter();
        const route = useRoute();
        
        // 编程式导航
        const goToHome = () => {
            router.push('/');
            // 或 router.push({ name: 'Home' });
            // 或 router.push({ path: '/' });
        };
        
        const goBack = () => {
            router.go(-1);
        };
        
        const replaceRoute = () => {
            router.replace('/about');
        };
        
        // 路由参数
        const userId = computed(() => route.params.id);
        const queryParam = computed(() => route.query.search);
        
        // 路由守卫
        onBeforeRouteUpdate((to, from) => {
            // 当前路由改变,但是该组件被复用时调用
            console.log('路由更新', to.params.id);
        });
        
        onBeforeRouteLeave((to, from) => {
            // 导航离开该组件的对应路由时调用
            const answer = window.confirm('确定要离开吗?');
            if (!answer) return false;
        });
        
        return {
            goToHome,
            goBack,
            replaceRoute,
            userId,
            queryParam
        };
    }
};
<!-- 模板中使用 -->
<template>
    <!-- 路由链接 -->
    <router-link to="/">首页</router-link>
    <router-link :to="{ name: 'About' }">关于</router-link>
    <router-link :to="{ path: '/user/123' }">用户123</router-link>
    <router-link 
        :to="{ 
            name: 'User', 
            params: { id: 456 },
            query: { tab: 'profile' },
            hash: '#section'
        }"
        custom
        v-slot="{ href, route, navigate, isActive, isExactActive }"
    >
        <a 
            :href="href" 
            @click="navigate"
            :class="{ active: isActive }"
        >
            自定义链接
        </a>
    </router-link>
    
    <!-- 路由视图 -->
    <router-view></router-view>
    
    <!-- 命名视图 -->
    <router-view name="header"></router-view>
    <router-view></router-view>
    <router-view name="footer"></router-view>
    
    <!-- 过渡效果 -->
    <router-view v-slot="{ Component }">
        <transition name="fade" mode="out-in">
            <component :is="Component" />
        </transition>
    </router-view>
</template>

7.3 路由元信息

// 路由配置中使用 meta
const routes = [
    {
        path: '/admin',
        meta: {
            requiresAuth: true,
            requiresAdmin: true,
            breadcrumb: '管理后台',
            transition: 'slide'
        },
        children: [
            {
                path: 'dashboard',
                component: Dashboard,
                meta: {
                    breadcrumb: '仪表板',
                    icon: 'dashboard'
                }
            }
        ]
    }
];

// 在导航守卫中使用
router.beforeEach((to, from, next) => {
    // 检查是否需要身份验证
    if (to.matched.some(record => record.meta.requiresAuth)) {
        if (!auth.loggedIn()) {
            next({
                path: '/login',
                query: { redirect: to.fullPath }
            });
        } else if (to.matched.some(record => record.meta.requiresAdmin)) {
            if (!auth.isAdmin()) {
                next({ path: '/unauthorized' });
            } else {
                next();
            }
        } else {
            next();
        }
    } else {
        next();
    }
});

// 在组件中访问
export default {
    setup() {
        const route = useRoute();
        
        // 获取当前路由的 meta
        const meta = computed(() => {
            return route.meta;
        });
        
        // 获取匹配的所有路由记录的 meta
        const matchedMetas = computed(() => {
            return route.matched.map(record => record.meta);
        });
        
        return { meta, matchedMetas };
    }
};

7.4 动态路由

// 添加路由
const router = createRouter({ /* ... */ });

// 动态添加路由
const adminRoutes = [
    {
        path: '/admin',
        component: AdminLayout,
        children: [
            { path: 'users', component: UserList },
            { path: 'settings', component: Settings }
        ]
    }
];

// 添加路由
adminRoutes.forEach(route => {
    router.addRoute(route);
});

// 添加嵌套路由
router.addRoute('admin', {
    path: 'profile',
    component: AdminProfile
});

// 删除路由
const removeRoute = router.addRoute(adminRoute);
removeRoute(); // 删除路由

// 或通过名称删除
router.removeRoute('admin');

// 检查路由是否存在
if (!router.hasRoute('admin')) {
    router.addRoute(adminRoute);
}

// 获取路由列表
const routeRecords = router.getRoutes();
console.log(routeRecords);

7.5 路由懒加载

// 1. 动态 import
const Home = () => import('./views/Home.vue');

// 2. 分组(webpack 魔法注释)
const UserProfile = () => import(/* webpackChunkName: "user" */ './views/UserProfile.vue');
const UserSettings = () => import(/* webpackChunkName: "user" */ './views/UserSettings.vue');

// 3. Vue 3 的 defineAsyncComponent
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() =>
    import('./components/AsyncComponent.vue')
);

// 4. 高级懒加载配置
const LazyComponent = defineAsyncComponent({
    loader: () => import('./HeavyComponent.vue'),
    loadingComponent: LoadingSpinner,
    errorComponent: ErrorComponent,
    delay: 200, // 延迟显示加载组件
    timeout: 3000, // 超时时间
    suspensible: false // 是否使用 Suspense
});

// 路由配置中使用
const routes = [
    {
        path: '/dashboard',
        component: () => import(/* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue')
    }
];

八、Vue 生态和工具

8.1 构建工具

  1. Vite

     // vite.config.js
     import { defineConfig } from 'vite';
     import vue from '@vitejs/plugin-vue';
     import { resolve } from 'path';
    
     export default defineConfig({
         plugins: [vue()],
            
         // 解析
         resolve: {
             alias: {
                 '@': resolve(__dirname, 'src')
             }
         },
            
         // 开发服务器
         server: {
             port: 3000,
             open: true,
             proxy: {
                 '/api': {
                     target: 'http://localhost:8080',
                     changeOrigin: true
                 }
             }
         },
            
         // 构建配置
         build: {
             outDir: 'dist',
             sourcemap: true,
             rollupOptions: {
                 output: {
                     manualChunks: {
                         vendor: ['vue', 'vue-router', 'pinia'],
                         ui: ['element-plus']
                     }
                 }
             }
         },
            
         // 环境变量
         envPrefix: 'VUE_'
     });
    
  2. Vue CLI

     // vue.config.js
     module.exports = {
         // 基本路径
         publicPath: process.env.NODE_ENV === 'production' ? '/production-sub-path/' : '/',
            
         // 输出目录
         outputDir: 'dist',
            
         // 静态资源目录
         assetsDir: 'static',
            
         // 开发服务器
         devServer: {
             port: 8080,
             proxy: {
                 '/api': {
                     target: 'http://localhost:3000',
                     changeOrigin: true
                 }
             }
         },
            
         // webpack 配置
         configureWebpack: {
             plugins: [],
             resolve: {
                 alias: {
                     '@': require('path').join(__dirname, 'src')
                 }
             }
         },
            
         // 链式配置
         chainWebpack: config => {
             config.plugin('html').tap(args => {
                 args[0].title = 'Vue App';
                 return args;
             });
         },
            
         // 生产环境 source map
         productionSourceMap: false,
            
         // CSS 相关配置
         css: {
             extract: true,
             sourceMap: false,
             loaderOptions: {
                 sass: {
                     additionalData: `@import "@/styles/variables.scss";`
                 }
             }
         }
     };
    

8.2 UI 组件库

  1. Element Plus

     // 安装
     npm install element-plus
    
     // 完整引入
     import ElementPlus from 'element-plus';
     import 'element-plus/dist/index.css';
    
     app.use(ElementPlus);
    
     // 按需引入
     import { createApp } from 'vue';
     import { ElButton, ElSelect } from 'element-plus';
    
     const app = createApp(App);
     app.component(ElButton.name, ElButton);
     app.component(ElSelect.name, ElSelect);
    
  2. Ant Design Vue

     // 安装
     npm install ant-design-vue@next
    
     // 使用
     import { createApp } from 'vue';
     import Antd from 'ant-design-vue';
     import 'ant-design-vue/dist/antd.css';
    
     const app = createApp(App);
     app.use(Antd);
    
  3. Vuetify

     // 安装
     npm install vuetify@^3.0.0
    
     // 使用
     import { createApp } from 'vue';
     import { createVuetify } from 'vuetify';
     import 'vuetify/styles';
    
     const vuetify = createVuetify({
         theme: {
             defaultTheme: 'light',
             themes: {
                 light: {
                     colors: {
                         primary: '#1867C0',
                         secondary: '#5CBBF6'
                     }
                 }
             }
         }
     });
    
     app.use(vuetify);
    

8.3 工具库

// 1. VueUse - 组合式工具集合
import { 
    useMouse, 
    useLocalStorage, 
    useFetch,
    useClipboard,
    useDark
} from '@vueuse/core';

// 2. lodash-es - 实用工具
import { debounce, throttle, cloneDeep } from 'lodash-es';

// 3. dayjs - 日期处理
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);

// 4. axios - HTTP 客户端
import axios from 'axios';

// 创建实例
const api = axios.create({
    baseURL: '/api',
    timeout: 10000
});

// 请求拦截器
api.interceptors.request.use(config => {
    const token = localStorage.getItem('token');
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
});

// 响应拦截器
api.interceptors.response.use(
    response => response.data,
    error => {
        if (error.response.status === 401) {
            // 处理未授权
        }
        return Promise.reject(error);
    }
);

8.4 测试工具

// 1. Vitest - 单元测试
// vitest.config.js
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [vue()],
    test: {
        globals: true,
        environment: 'jsdom'
    }
});

// 测试示例
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';

describe('Counter', () => {
    it('increments count', async () => {
        const wrapper = mount(Counter);
        await wrapper.find('button').trigger('click');
        expect(wrapper.text()).toContain('Count: 1');
    });
});

// 2. Cypress - E2E 测试
// cypress/e2e/counter.cy.js
describe('Counter', () => {
    it('increments count', () => {
        cy.visit('/');
        cy.contains('button', '增加').click();
        cy.contains('Count: 1');
    });
});

// 3. Vue Test Utils
import { shallowMount } from '@vue/test-utils';
import MyComponent from './MyComponent.vue';

describe('MyComponent', () => {
    it('renders props', () => {
        const wrapper = shallowMount(MyComponent, {
            props: { msg: 'Hello' }
        });
        expect(wrapper.text()).toContain('Hello');
    });
});

8.5 代码质量工具

// 1. ESLint
// .eslintrc.js
module.exports = {
    root: true,
    env: {
        node: true,
        browser: true
    },
    extends: [
        'plugin:vue/vue3-essential',
        'eslint:recommended',
        '@vue/typescript/recommended'
    ],
    parserOptions: {
        ecmaVersion: 2020
    },
    rules: {
        'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        'vue/multi-word-component-names': 'off'
    }
};

// 2. Prettier
// .prettierrc
{
    "semi": true,
    "tabWidth": 4,
    "singleQuote": true,
    "trailingComma": "none",
    "printWidth": 100
}

// 3. Stylelint
// .stylelintrc
{
    "extends": [
        "stylelint-config-standard",
        "stylelint-config-recommended-vue"
    ],
    "rules": {
        "selector-class-pattern": null
    }
}

// 4. Husky + lint-staged
// package.json
{
    "scripts": {
        "prepare": "husky install"
    },
    "lint-staged": {
        "*.{js,ts,vue}": ["eslint --fix", "prettier --write"],
        "*.{css,scss}": ["stylelint --fix", "prettier --write"]
    }
}

九、服务端渲染(SSR)和静态站点生成(SSG)

9.1 Nuxt.js

// nuxt.config.js
export default defineNuxtConfig({
    // 应用配置
    app: {
        head: {
            title: 'Nuxt 3 应用',
            meta: [
                { charset: 'utf-8' },
                { name: 'viewport', content: 'width=device-width, initial-scale=1' }
            ],
            link: [
                { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
            ]
        }
    },
    
    // 模块
    modules: [
        '@nuxtjs/tailwindcss',
        '@pinia/nuxt',
        '@nuxt/image'
    ],
    
    // 构建模块
    buildModules: [
        '@nuxt/typescript-build'
    ],
    
    // 运行时配置
    runtimeConfig: {
        public: {
            apiBase: process.env.API_BASE || 'http://localhost:3000'
        }
    },
    
    // 渲染模式
    ssr: true,  // 服务端渲染
    
    // 目标
    target: 'static',  // 静态站点生成
    
    // 路由
    router: {
        middleware: ['auth']
    },
    
    // 插件
    plugins: [
        '~/plugins/axios.js'
    ],
    
    // 构建配置
    build: {
        extractCSS: true,
        transpile: ['some-package']
    }
});

9.2 Vue 服务端渲染

// server.js - 基本的 SSR 实现
import express from 'express';
import { createSSRApp } from 'vue';
import { renderToString } from '@vue/server-renderer';

const server = express();

server.get('*', async (req, res) => {
    // 创建 Vue 应用实例
    const app = createSSRApp({
        data: () => ({ url: req.url }),
        template: `<div>访问的 URL 是: {{  url }} </div>`
    });
    
    // 渲染为 HTML
    const appContent = await renderToString(app);
    
    // 完整的 HTML
    const html = `
        <!DOCTYPE html>
        <html>
            <head>
                <title>Vue SSR</title>
            </head>
            <body>
                <div id="app">${appContent}</div>
                <script>
                    // 客户端激活
                    window.__INITIAL_STATE__ = ${JSON.stringify({ url: req.url })}
                </script>
                <script src="/client.js"></script>
            </body>
        </html>
    `;
    
    res.send(html);
});

server.listen(3000, () => {
    console.log('Server running on http://localhost:3000');
});

9.3 静态站点生成

// 使用 VitePress
// .vitepress/config.js
module.exports = {
    title: 'VitePress',
    description: 'Vite & Vue powered static site generator.',
    
    themeConfig: {
        nav: [
            { text: '指南', link: '/guide/' },
            { text: '配置', link: '/config/' }
        ],
        
        sidebar: {
            '/guide/': [
                {
                    text: '指南',
                    children: [
                        { text: '介绍', link: '/guide/' },
                        { text: '开始', link: '/guide/getting-started' }
                    ]
                }
            ]
        }
    },
    
    // 构建配置
    build: {
        outDir: '../dist'
    }
};

9.4 混合渲染

// Nuxt.js 混合渲染
export default defineNuxtConfig({
    routeRules: {
        // 首页预渲染
        '/': { prerender: true },
        
        // 产品页静态生成
        '/products/**': { static: true },
        
        // 用户页服务端渲染
        '/users/**': { ssr: true },
        
        // API 路由
        '/api/**': { cors: true },
        
        // 动态路由
        '/blog/**': { swr: 3600 } // 缓存 1 小时
    }
});

十、Vue 3 新特性

10.1 组合式 API

// 1. setup 语法糖
<script setup>
import { ref, computed, onMounted } from 'vue';

// 响应式状态
const count = ref(0);
const name = ref('Vue');

// 计算属性
const doubleCount = computed(() => count.value * 2);

// 方法
const increment = () => {
    count.value++;
};

// 生命周期
onMounted(() => {
    console.log('组件已挂载');
});

// 自动暴露给模板
</script>

<template>
    <button @click="increment"> {{  count }} </button>
</template>
// 2. 响应性系统改进
import { 
    reactive, 
    ref, 
    readonly, 
    shallowReactive, 
    shallowRef,
    toRef,
    toRefs,
    customRef
} from 'vue';

// 自定义 ref
function useDebouncedRef(value, delay = 200) {
    let timeout;
    return customRef((track, trigger) => {
        return {
            get() {
                track();
                return value;
            },
            set(newValue) {
                clearTimeout(timeout);
                timeout = setTimeout(() => {
                    value = newValue;
                    trigger();
                }, delay);
            }
        };
    });
}

// 在组件中使用
const text = useDebouncedRef('hello');

10.2 Teleport

<template>
    <!-- 将组件传送到 body -->
    <teleport to="body">
        <div v-if="showModal" class="modal">
            <h2>模态框标题</h2>
            <button @click="showModal = false">关闭</button>
        </div>
    </teleport>
    
    <!-- 多个目标 -->
    <teleport to="#modals">
        <div>模态框 1</div>
    </teleport>
    <teleport to="#modals">
        <div>模态框 2</div>
    </teleport>
    
    <!-- 条件传送 -->
    <teleport :to="target">
        <div>动态目标</div>
    </teleport>
</template>

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

const showModal = ref(false);
const target = ref('#modals');
</script>

10.3 Fragments

<!-- Vue 3 支持多个根元素 -->
<template>
    <header>头部</header>
    <main>内容</main>
    <footer>底部</footer>
</template>

<!-- 不需要包裹 div -->

10.4 Emits 选项

<script setup>
// 定义 emits
const emit = defineEmits(['update:modelValue', 'submit']);

// 触发事件
const handleSubmit = () => {
    emit('submit', formData);
};
</script>

<!-- 或使用对象语法 -->
<script>
export default {
    emits: {
        // 没有验证
        click: null,
        
        // 带验证
        submit: (payload) => {
            if (payload.email && payload.password) {
                return true;
            } else {
                console.warn('Submit 事件无效');
                return false;
            }
        }
    }
};
</script>

10.5 自定义渲染器

// 创建自定义渲染器
import { createRenderer } from '@vue/runtime-core';

const { render, createApp } = createRenderer({
    // 平台特定的 API
    insert: (child, parent, anchor) => {
        // 插入元素
    },
    remove: (child) => {
        // 移除元素
    },
    createElement: (tag, isSVG, is) => {
        // 创建元素
    },
    setElementText: (el, text) => {
        // 设置文本
    },
    // ... 其他节点操作
});

// 创建 Canvas 渲染器示例
const { createApp: createCanvasApp } = createRenderer({
    createElement(type) {
        return { type };
    },
    patchProp(el, key, prevValue, nextValue) {
        // 处理属性
    },
    insert(el, parent) {
        // 绘制到 Canvas
    }
});

10.6 全局 API 变更

// Vue 2
import Vue from 'vue';
Vue.component('MyComponent', MyComponent);
Vue.directive('focus', focusDirective);
Vue.mixin({ /* ... */ });
Vue.use(MyPlugin);

// Vue 3
import { createApp } from 'vue';

const app = createApp({});
app.component('MyComponent', MyComponent);
app.directive('focus', focusDirective);
app.mixin({ /* ... */ });
app.use(MyPlugin);
app.mount('#app');

10.7 其他新特性

  1. Suspense

     <template>
         <Suspense>
             <template #default>
                 <AsyncComponent />
             </template>
             <template #fallback>
                 <div>加载中...</div>
             </template>
         </Suspense>
     </template>
    
  2. v-model 参数

     <template>
         <!-- Vue 2: 一个组件只能有一个 v-model -->
         <!-- Vue 3: 多个 v-model -->
         <CustomInput v-model:first-name="firstName" v-model:last-name="lastName" />
            
         <!-- 自定义修饰符 -->
         <CustomInput v-model.capitalize="text" />
     </template>
    
  3. 样式特性

     <style scoped>
     /* 深度选择器 */
     :deep(.child) {
         color: red;
     }
    
     /* 插槽选择器 */
     :slotted(div) {
         color: blue;
     }
    
     /* 全局选择器 */
     :global(.global-class) {
         font-weight: bold;
     }
     </style>
    

十一、性能优化

11.1 代码分割

// 1. 路由懒加载
const router = createRouter({
    routes: [
        {
            path: '/dashboard',
            component: () => import('./views/Dashboard.vue')
        }
    ]
});

// 2. 组件懒加载
import { defineAsyncComponent } from 'vue';

const HeavyComponent = defineAsyncComponent(() =>
    import('./HeavyComponent.vue')
);

// 3. 第三方库分割
// vite.config.js
export default defineConfig({
    build: {
        rollupOptions: {
            output: {
                manualChunks: {
                    'vue-vendor': ['vue', 'vue-router', 'pinia'],
                    'ui-library': ['element-plus']
                }
            }
        }
    }
});

11.2 虚拟滚动

// 使用 vue-virtual-scroller
import { RecycleScroller } from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';

export default {
    components: {
        RecycleScroller
    },
    data() {
        return {
            items: Array.from({ length: 10000 }, (_, i) => ({
                id: i,
                name: `项目 ${i}`
            }))
        };
    }
};
<template>
    <RecycleScroller
        class="scroller"
        :items="items"
        :item-size="50"
        key-field="id"
    >
        <template v-slot="{ item }">
            <div class="item"> {{  item.name }} </div>
        </template>
    </RecycleScroller>
</template>

<style>
.scroller {
    height: 400px;
}
.item {
    height: 50px;
    line-height: 50px;
    border-bottom: 1px solid #eee;
}
</style>

11.3 图片优化

// 使用 vue-lazyload
import { createApp } from 'vue';
import VueLazyload from 'vue-lazyload';

const app = createApp(App);
app.use(VueLazyload, {
    preLoad: 1.3,
    error: 'error.png',
    loading: 'loading.gif',
    attempt: 1
});
<!-- 图片懒加载 -->
<img v-lazy="imageUrl" />

<!-- 响应式图片 -->
<picture>
    <source media="(min-width: 1200px)" :srcset="largeImage" />
    <source media="(min-width: 768px)" :srcset="mediumImage" />
    <img :src="smallImage" alt="描述" />
</picture>

<!-- WebP 格式 -->
<picture>
    <source type="image/webp" :srcset="webpImage" />
    <source type="image/jpeg" :srcset="jpegImage" />
    <img :src="jpegImage" alt="描述" />
</picture>

11.4 组件优化

// 1. 函数式组件
const FunctionalButton = (props, context) => {
    return h('button', {
        onClick: props.onClick
    }, context.slots.default());
};

// 2. 异步组件
const AsyncComponent = defineAsyncComponent(() =>
    import('./AsyncComponent.vue')
);

// 3. KeepAlive
<KeepAlive :include="['Home', 'About']" :max="10">
    <component :is="currentComponent" />
</KeepAlive>

// 4. v-once
<span v-once> {{  staticContent }} </span>

// 5. v-memo
<div v-memo="[valueA, valueB]">
    <!-- 只在 valueA  valueB 变化时更新 -->
</div>

11.5 性能监控

// 性能测量
import { onMounted, onUnmounted } from 'vue';

export default {
    setup() {
        let startTime;
        
        onMounted(() => {
            startTime = performance.now();
            
            // 监听长任务
            const observer = new PerformanceObserver((list) => {
                for (const entry of list.getEntries()) {
                    if (entry.duration > 50) {
                        console.warn('长任务:', entry);
                    }
                }
            });
            
            observer.observe({ entryTypes: ['longtask'] });
        });
        
        onUnmounted(() => {
            const loadTime = performance.now() - startTime;
            console.log(`组件渲染时间: ${loadTime}ms`);
            
            // 发送到监控服务
            sendToAnalytics({ loadTime });
        });
    }
};

11.6 构建优化

// vite.config.js
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
    plugins: [
        // 分析包大小
        visualizer({
            open: true,
            gzipSize: true,
            brotliSize: true
        })
    ],
    
    build: {
        // 压缩配置
        minify: 'terser',
        terserOptions: {
            compress: {
                drop_console: true,
                drop_debugger: true
            }
        },
        
        // 资源优化
        assetsInlineLimit: 4096, // 4KB 以下内联
        
        // 代码分割
        rollupOptions: {
            output: {
                manualChunks: {
                    vendor: ['vue', 'vue-router', 'pinia']
                }
            }
        }
    },
    
    // 预构建
    optimizeDeps: {
        include: ['vue', 'vue-router']
    }
});

十二、测试

12.1 单元测试

// Counter.vue
<script setup>
import { ref } from 'vue';

const count = ref(0);
const increment = () => {
    count.value++;
};
</script>

<template>
    <button @click="increment">点击  {{  count }} </button>
</template>
// Counter.spec.js
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';

describe('Counter.vue', () => {
    it('渲染初始状态', () => {
        const wrapper = mount(Counter);
        expect(wrapper.text()).toContain('点击 0');
    });
    
    it('点击按钮增加计数', async () => {
        const wrapper = mount(Counter);
        await wrapper.find('button').trigger('click');
        expect(wrapper.text()).toContain('点击 1');
    });
    
    it('触发自定义事件', async () => {
        const wrapper = mount(Counter, {
            props: {
                initialCount: 5
            }
        });
        
        expect(wrapper.text()).toContain('点击 5');
    });
});

12.2 组件测试

// 测试组件 props
it('接收 props', () => {
    const wrapper = mount(Component, {
        props: {
            msg: 'Hello'
        }
    });
    expect(wrapper.props('msg')).toBe('Hello');
});

// 测试组件事件
it('触发事件', async () => {
    const wrapper = mount(Component);
    await wrapper.find('button').trigger('click');
    expect(wrapper.emitted('submit')).toBeTruthy();
});

// 测试插槽
it('渲染插槽内容', () => {
    const wrapper = mount(Component, {
        slots: {
            default: '插槽内容'
        }
    });
    expect(wrapper.text()).toContain('插槽内容');
});

// 测试异步行为
it('处理异步操作', async () => {
    const wrapper = mount(AsyncComponent);
    await wrapper.vm.$nextTick();
    expect(wrapper.text()).toContain('加载完成');
});

12.3 集成测试

// 测试组件组合
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createRouter, createWebHistory } from 'vue-router';
import { createPinia } from 'pinia';
import App from './App.vue';

const router = createRouter({
    history: createWebHistory(),
    routes: [{ path: '/', component: { template: '<div>Home</div>' } }]
});

const pinia = createPinia();

describe('App 集成测试', () => {
    it('完整应用渲染', async () => {
        const wrapper = mount(App, {
            global: {
                plugins: [router, pinia]
            }
        });
        
        expect(wrapper.html()).toContain('div');
    });
});

12.4 E2E 测试

// Cypress 测试
describe('用户登录流程', () => {
    it('成功登录', () => {
        cy.visit('/login');
        cy.get('[data-test="username"]').type('user@example.com');
        cy.get('[data-test="password"]').type('password123');
        cy.get('[data-test="submit"]').click();
        cy.url().should('include', '/dashboard');
        cy.get('[data-test="welcome"]').should('contain', 'Welcome');
    });
    
    it('登录失败显示错误', () => {
        cy.visit('/login');
        cy.get('[data-test="username"]').type('wrong@example.com');
        cy.get('[data-test="password"]').type('wrong');
        cy.get('[data-test="submit"]').click();
        cy.get('[data-test="error"]').should('be.visible');
    });
});

12.5 测试工具配置

// vitest.config.js
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [vue()],
    test: {
        globals: true,
        environment: 'jsdom',
        coverage: {
            provider: 'c8',
            reporter: ['text', 'json', 'html']
        },
        setupFiles: ['./tests/setup.js']
    }
});

// tests/setup.js
import { config } from '@vue/test-utils';

// 全局配置
config.global.stubs = {
    Transition: false,
    'transition-group': false
};

// 全局组件
config.global.components = {
    'RouterLink': { template: '<a><slot /></a>' }
};

十三、TypeScript 支持

13.1 基本类型

// 组件 Props 类型
interface Props {
    msg: string;
    count?: number;
    items: string[];
    user: {
        name: string;
        age: number;
    };
    onClick: (event: MouseEvent) => void;
}

// 定义组件
import { defineComponent } from 'vue';

export default defineComponent({
    props: {
        msg: {
            type: String as PropType<string>,
            required: true
        },
        count: {
            type: Number as PropType<number>,
            default: 0
        }
    },
    setup(props: Props) {
        // props 是类型安全的
        console.log(props.msg.toLowerCase());
    }
});

13.2 组合式 API 类型

<script setup lang="ts">
import { ref, computed, Ref } from 'vue';

// 响应式引用
const count: Ref<number> = ref(0);

// 计算属性
const doubleCount = computed(() => count.value * 2);

// 函数
const increment = (): void => {
    count.value++;
};

// 接口定义
interface User {
    id: number;
    name: string;
    email: string;
}

const user: Ref<User | null> = ref(null);
</script>

13.3 组件类型

// 1. 使用 defineComponent
import { defineComponent, PropType } from 'vue';

interface Book {
    title: string;
    author: string;
    year: number;
}

export default defineComponent({
    props: {
        book: {
            type: Object as PropType<Book>,
            required: true
        },
        isFavorite: {
            type: Boolean,
            default: false
        }
    },
    
    emits: {
        'update:favorite': (value: boolean) => true
    },
    
    setup(props, { emit }) {
        const toggleFavorite = () => {
            emit('update:favorite', !props.isFavorite);
        };
        
        return { toggleFavorite };
    }
});

13.4 全局类型

// types/vue.d.ts
import { ComponentCustomProperties } from 'vue';

declare module 'vue' {
    interface ComponentCustomProperties {
        $filters: {
            formatDate: (date: Date) => string;
            formatCurrency: (amount: number) => string;
        };
    }
}

// 在组件中使用
export default {
    mounted() {
        console.log(this.$filters.formatDate(new Date()));
    }
};

13.5 Pinia 类型

// store/user.ts
import { defineStore } from 'pinia';

interface UserState {
    name: string;
    age: number;
    isLoggedIn: boolean;
}

export const useUserStore = defineStore('user', {
    state: (): UserState => ({
        name: '',
        age: 0,
        isLoggedIn: false
    }),
    
    getters: {
        isAdult(): boolean {
            return this.age >= 18;
        },
        
        userInfo(): string {
            return `${this.name} (${this.age})`;
        }
    },
    
    actions: {
        async login(username: string, password: string): Promise<boolean> {
            // 登录逻辑
            return true;
        }
    }
});

13.6 Vue Router 类型

// router/index.ts
import { RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
    {
        path: '/',
        name: 'Home',
        component: () => import('@/views/Home.vue'),
        meta: {
            requiresAuth: true
        }
    }
];

// 扩展 RouteMeta
declare module 'vue-router' {
    interface RouteMeta {
        requiresAuth?: boolean;
        title?: string;
        roles?: string[];
    }
}

十四、学习资源

14.1 官方资源

  • Vue.js 官网
  • Vue.js 中文官网
  • Vue Router 文档
  • Pinia 文档

十四、学习资源

14.1 官方资源

14.2 教程与课程

  • Vue.js 官方教程:包含交互式练习,是入门的最佳起点。
  • Vue Mastery:高质量的付费视频课程,涵盖从基础到高级的各个主题。
  • Vue School:提供免费和付费课程,内容更新及时。
  • freeCodeCamp:提供免费的 Vue.js 响应式网页设计认证课程。
  • YouTube 频道
    • Vue.js (官方):发布核心团队分享和新特性介绍。
    • Vue Mastery / Vue School:课程节选和技巧分享。
    • Traversy Media, The Net Ninja:包含丰富的 Vue 项目实战教程。

14.3 推荐书籍

  1. 入门与实战
    • 《Vue.js 设计与实现》- 霍春阳
      • 深入解析 Vue 3 的响应式系统、编译器和运行时核心,适合希望深入理解原理的开发者。
    • 《Vue.js 项目实战》- 系列实战书籍
      • 通过完整的项目案例学习 Vue 的全栈开发。
  2. 深入原理
    • 《深入浅出 Vue.js》- 刘博文
      • 针对 Vue 2 的源码进行深入剖析,是理解 Vue 设计思想的经典之作。

14.4 社区与资讯

  • GitHub:关注 vuejs 组织下的核心项目(core, router, pinia, vite)及热门 UI 库。
  • 掘金/思否:中文开发者社区,有大量 Vue 相关的技术文章、实战经验和问题讨论。
  • Discord / 论坛:Vue 官方社区,是获取帮助和与社区交流的直接渠道。
  • 博客与周刊
    • Vue.js Blog:官方博客,发布版本更新和新特性介绍。
    • Vue.js Developers Newsletter:订阅获取每周精选的 Vue 文章和资源。
    • 新闻:关注前端和 Vue 生态的最新动态。

14.5 工具与生态

  1. 浏览器开发工具
    • Vue DevTools:浏览器扩展,用于调试 Vue 应用组件树、状态和事件。
  2. IDE/编辑器插件
    • Volar:VS Code 官方推荐的 Vue 语言支持插件,对 Vue 3 和 TypeScript 提供一流支持。
    • Vue VSCode Snippets:提供丰富的代码片段,提升开发效率。
  3. UI 组件库
    • Element Plus:基于 Vue 3,面向桌面端的组件库。
    • Ant Design Vue:企业级 UI 设计语言和 React 实现 的 Vue 版本。
    • Vuetify:Material Design 风格的组件框架。
    • Naive UI:一个全量的 Vue 3 组件库,比较完整,主题可调,使用 TypeScript 开发。
  4. 实用工具库
    • VueUse:一个基于 Composition API 的实用函数集合,极大地提高了开发效率。
    • Vue-i18n:国际化插件。
    • Vue Router:官方路由管理器。

14.6 学习建议与路径

  1. 基础阶段 (1-2周)
    • 完成 Vue 官方指南,掌握模板语法、响应式基础、计算属性、侦听器、条件与列表渲染、事件处理、表单输入绑定、组件基础。
    • 在 CodePen、CodeSandbox 或本地使用 Vite 创建项目进行实践。
  2. 核心进阶 (2-3周)
    • 深入学习组件(Props、自定义事件、插槽)、生命周期、组合式 API (setup, ref, reactive, computed, watch)。
    • 学习 Vue Router 实现页面路由,学习 Pinia 进行状态管理。
  3. 工程化与实战 (3-4周)
    • 掌握 Vite 构建工具,学习使用 TypeScript 增强代码健壮性。
    • 选择一个 UI 组件库,并尝试开发一个中等复杂度的全栈项目(如后台管理系统、博客平台)。
    • 学习单元测试 (Vitest) 和端到端测试 (Cypress)。
  4. 持续深入
    • 关注 Vue 和 Vite 的 RFC(征求意见稿),了解生态发展趋势。
    • 研究 Nuxt.js 等服务端渲染/静态站点生成框架。
    • 阅读优秀开源项目源码,参与社区讨论和贡献。

Tags:

Categories:

Updated: