WinddSnow

Vue-Options-API-vs-Composition-API-Migration-Mixins-to-Composables

字数统计: 5.1k阅读时长: 21 min
2026/08/01

第112课:选项式 API 对比与迁移——代码组织、逻辑复用、mixins 迁移到组合式

Vue 从诞生之初就使用选项式 API:将组件逻辑按选项类型(datamethodscomputedwatch 等)组织。这种方式直观、易于上手,但随着组件复杂度的增长,同一逻辑关注点的代码被拆分到不同选项中,形成“逻辑碎片化”。Vue 3 引入的组合式 API 解决了这一问题——将同一功能的所有响应式状态、计算属性和副作用集中在 setup 函数或 <script setup> 中,甚至可以提取为独立的 Composable(组合函数)在多个组件间复用。对于从 Vue 2 迁移过来的项目,最大的挑战是如何将大量基于 mixins 的复用逻辑安全、高效地迁移到组合式模式。本节课将系统对比两种 API 的代码组织方式、揭示 mixins 的痛点(命名冲突、来源不透明、耦合),并通过完整示例展示从 mixins 到 Composable 的迁移步骤和最佳实践。


1. 选项式 API 与组合式 API 的代码组织对比

1.1 选项式 API 的逻辑碎片问题

在选项式 API 中,组件逻辑按照 datamethodscomputedwatchmounted 等选项分组。这在逻辑简单时非常清晰,但当组件包含多个功能模块时(如“搜索”、“分页”、“数据获取”),每个模块的代码被分散在不同选项中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- 选项式 API:搜索 + 分页 + 数据获取混合在一个组件中 -->
<script>
export default {
data() {
return {
query: '', // 搜索相关
page: 1, // 分页相关
items: [], // 数据相关
loading: false,
};
},
computed: {
// 无法区分哪个计算属性属于哪个功能
filteredItems() { /* ... */ },
totalPages() { /* ... */ },
},
watch: {
query() { this.fetchData(); },
page() { this.fetchData(); },
},
methods: {
search(q) { this.query = q; },
goToPage(p) { this.page = p; },
async fetchData() { /* ... */ },
},
mounted() {
this.fetchData();
},
};
</script>

当组件体积增大时,这种“按选项分类”的组织方式导致开发者需要在不同选项之间反复跳转,难以快速理解某个功能的完整逻辑。

1.2 组合式 API 的功能内聚

组合式 API 允许将同一功能的所有变量和方法就近定义,形成内聚的代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 组合式 API:同一功能的代码集中在一起 -->
<script setup>
import { ref, computed, watch, onMounted } from 'vue';

// ---- 搜索功能 ----
const query = ref('');
function search(q) { query.value = q; }

// ---- 分页功能 ----
const page = ref(1);
function goToPage(p) { page.value = p; }

// ---- 数据获取 ----
const items = ref([]);
const loading = ref(false);
async function fetchData() {
loading.value = true;
items.value = await fetch(`/api?q=${query.value}&page=${page.value}`).then(r => r.json());
loading.value = false;
}
watch([query, page], fetchData);
onMounted(fetchData);
</script>

更重要的是,每个功能块可以被提取为独立的 Composable 函数(见第 3 节),实现跨组件的逻辑复用——这是组合式 API 相比选项式 API 最根本的优势。

1.3 两种 API 的全面对比

维度 选项式 API 组合式 API
代码组织 按选项类型分组(data、methods、computed 等)。 按功能关注点分组,同一功能代码内聚。
逻辑复用 依赖 mixins(存在命名冲突、来源不透明问题)。 使用 Composable 函数,显式导入,来源清晰。
TypeScript 支持 需要额外类型标注,this 上下文类型推断较弱。 原生支持,ref/computed 自动推导类型。
生命周期 选项名称(mountedupdated 等)。 组合式函数(onMountedonUpdated 等)。
响应式系统 基于 this 访问,data() 返回对象。 使用 ref/reactive,可自由控制响应式边界。
学习曲线 直观,适合新手。 需要理解 refreactivewatch 的用法。
包体积 略微更小(无额外 runtime)。 略微更大(需要处理 setup 的 runtime)。

选择建议:新项目、TypeScript 项目、复杂组件优先使用组合式 API。简单组件、快速原型、喜欢传统编写风格的团队可继续使用选项式 API——Vue 3 完全兼容选项式 API,两者可以在同一项目中混合使用。


2. Mixins 的痛点:为什么需要迁移?

Mixins 是 Vue 2 中实现逻辑复用的主要手段。一个 mixin 对象可以包含 datamethodscomputedwatch 等选项,组件通过 mixins: [myMixin] 将其“混入”自身。

2.1 命名冲突

当多个 mixins 或组件自身定义了同名的属性或方法时,Vue 会按照一定的合并策略处理——组件自身的选项优先于 mixin 的选项。但这仍然无法解决开发者在阅读代码时面临的困扰:你看到的 this.fetchData() 究竟来自哪个 mixin?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// mixins/pagination.js
export default {
data() { return { page: 1 }; },
methods: { nextPage() { this.page++; } },
};

// mixins/search.js
export default {
data() { return { query: '' }; },
methods: {
// 与 pagination 的 nextPage 共存,但开发者容易混淆
search(q) { this.query = q; },
},
};

// 组件
export default {
mixins: [paginationMixin, searchMixin],
data() { return { page: 1 }; }, // 同名属性覆盖 mixin 中的 page
};

2.2 来源不透明

当组件混入了 3 个 mixins,每个 mixin 又可能混入了其他 mixins 时,组件的行为变得极难追踪。你无法从组件的代码中一眼看出某个方法或数据属性是从哪里来的——必须追溯整个 mixin 继承链。这在调试和重构大型项目时是巨大的障碍。

2.3 隐式依赖与紧耦合

一个 mixin 可能隐式依赖组件中的某个 data 属性(如 this.userId),而这种依赖在代码中没有明确的声明。组件修改了该属性名后,mixin 会静默地失效(访问 undefined),不产生任何错误提示。组合式 API 通过显式的函数参数解决了这一问题——所有依赖都是明确的。


3. 从 Mixins 迁移到 Composable:完整步骤

将 mixin 重构为 Composable 的过程是结构化的。以下是标准步骤:

  1. 创建一个以 use 开头的函数,将 mixin 中的 data 转换为 ref/reactivecomputed 转换为 computedmethods 转换为普通函数,watch 转换为 watch/watchEffect,生命周期钩子转换为 onMounted 等。
  2. 将 mixin 中对 this 的依赖改为参数传递:如果 mixin 原先依赖组件中的某个属性,将该属性作为 Composable 函数的参数传入。
  3. 在组件中调用 Composable 函数,解构或直接使用返回的变量和方法。
  4. 移除组件的 mixins 选项

3.1 案例:一个“窗口尺寸监听” mixin 的迁移

原 mixin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// mixins/windowSize.js
export default {
data() {
return {
windowWidth: window.innerWidth,
windowHeight: window.innerHeight,
};
},
methods: {
handleResize() {
this.windowWidth = window.innerWidth;
this.windowHeight = window.innerHeight;
},
},
mounted() {
window.addEventListener('resize', this.handleResize);
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
},
};

迁移后的 Composable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// composables/useWindowSize.js
import { ref, onMounted, onBeforeUnmount } from 'vue';

export function useWindowSize() {
const width = ref(window.innerWidth);
const height = ref(window.innerHeight);

function handleResize() {
width.value = window.innerWidth;
height.value = window.innerHeight;
}

onMounted(() => {
window.addEventListener('resize', handleResize);
});

onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
});

return { width, height };
}

在组件中使用

1
2
3
4
5
6
7
8
9
<script setup>
import { useWindowSize } from '@/composables/useWindowSize';

const { width, height } = useWindowSize();
</script>

<template>
<p>窗口尺寸:{{ width }} × {{ height }}</p>
</template>

迁移的关键改进

  • data 转换为 refmethods 转换为普通函数。
  • 原先隐式挂载在 this 上的状态现在通过 return 显式暴露,组件可以按需解构。
  • 来源完全透明——useWindowSize 的导入路径清晰可见。
  • 没有命名冲突:如果另一个 Composable 也返回 width,组件只需在解构时重命名:const { width: windowWidth } = useWindowSize()

3.2 处理依赖:从隐式 this 到显式参数

有些 mixins 依赖组件中的特定属性。迁移时需要将这些属性作为函数参数传入。

原 mixin(依赖 this.userId

1
2
3
4
5
6
7
8
9
10
11
12
export default {
data() { return { user: null }; },
methods: {
async fetchUser() {
this.user = await fetch(`/api/users/${this.userId}`).then(r => r.json());
},
},
watch: {
userId() { this.fetchUser(); },
},
mounted() { this.fetchUser(); },
};

迁移后的 Composable(userId 作为参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { ref, watch, onMounted, toRef } from 'vue';

export function useUser(userIdRef) {
const user = ref(null);
const loading = ref(false);

async function fetchUser() {
loading.value = true;
try {
user.value = await fetch(`/api/users/${userIdRef.value}`).then(r => r.json());
} finally {
loading.value = false;
}
}

watch(userIdRef, fetchUser, { immediate: true });

return { user, loading, fetchUser };
}

在组件中使用

1
2
3
4
5
6
7
<script setup>
import { ref } from 'vue';
import { useUser } from '@/composables/useUser';

const userId = ref(1);
const { user, loading } = useUser(userId);
</script>

userIdRef 可以是 refcomputed。通过 toRef 也可以将 Props 转为 ref 传入。依赖关系通过参数明确表达,不会再有“隐式耦合”。

3.3 处理生命周期钩子:直接在 Composable 中注册

Composable 函数中可以直接使用 onMountedonBeforeUnmount 等生命周期钩子。这些钩子会自动绑定到当前调用 Composable 的组件实例上。因此,Composable 不需要关心调用它的组件何时挂载或卸载——它只需要在 setup 期间注册钩子即可。

1
2
3
4
5
6
7
8
9
10
11
12
// composables/useInterval.js
import { onBeforeUnmount } from 'vue';

export function useInterval(callback, interval) {
const timer = setInterval(callback, interval);

onBeforeUnmount(() => {
clearInterval(timer);
});

// 无需返回 timer,清理逻辑已自动绑定
}

4. 多个 Mixins 到多个 Composables 的协作

当组件原先混入了多个 mixins,每个 mixin 可能依赖其他 mixin 提供的数据。迁移后的 Composables 通过参数传递返回值来协作,依赖关系清晰可见。

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { ref } from 'vue';
import { useUser } from '@/composables/useUser';
import { useOrders } from '@/composables/useOrders';

const userId = ref(1);

// useOrders 依赖 userId,也依赖 useUser 返回的 user
const { user } = useUser(userId);
const { orders, loading } = useOrders(userId, user); // 明确传递依赖
</script>

与 mixins 的隐式共享 this 相比,这种显式传递模式使得数据流一目了然,且类型安全。

4.1 避免过度解构导致响应式丢失

从 Composable 解构返回的 ref 时,如果直接解构为基本变量(如 const { width } = useWindowSize()),响应性会被保留——因为在 <script setup> 中,Vue 编译器会自动处理顶层解构。但如果在普通的 setup() 函数中或嵌套函数中解构,需要使用 toRefs 保持响应性。

1
2
3
4
5
6
7
8
// ✅ <script setup> 中:直接解构,响应性保留
const { width, height } = useWindowSize();

// ❌ 普通 setup() 中直接解构:失去响应性
setup() {
const { width, height } = useWindowSize(); // width 不再是 ref
// 需要改为:const size = useWindowSize(); 然后 size.width
}

在 Composable 设计中,一个常见模式是返回一个包含所有属性的对象(不强行解构),让调用方自行决定如何使用。


5. 混合项目中的渐进式迁移策略

对于仍在维护的大型 Vue 2 项目(已通过 @vue/compat 迁移到 Vue 3 或仍使用 Vue 2 + Composition API 插件),不可能一次性将所有 mixins 重写。推荐以下渐进式策略:

  1. 新功能全部使用 Composable:在新开发的组件和功能中,不再创建新的 mixin。所有复用逻辑通过 Composable 实现。
  2. 重构最复杂的 mixin:优先迁移那些导致最多 Bug 和困惑的 mixin。每迁移一个,就从中提取出清晰的 Composable,并移除组件中对应的 mixins 引用。
  3. 保留简单的 mixin:对于逻辑极其简单、无命名冲突风险的 mixin(如一个仅提供 computed 的全局主题变量),可以暂时保留,但应标记为待迁移。
  4. 使用适配层:在迁移期间,可以创建一个 Composable 包装器,内部调用原 mixin 的逻辑,对外提供 Composable 接口。这样新旧代码可以共存,逐步替换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 适配层:将旧 mixin 包装为 Composable
import { reactive, onMounted, onBeforeUnmount } from 'vue';

export function useLegacyFeature() {
// 创建一个“虚拟组件”来承载 mixin 的生命周期
const state = reactive({
windowWidth: window.innerWidth,
windowHeight: window.innerHeight,
});

function handleResize() {
state.windowWidth = window.innerWidth;
state.windowHeight = window.innerHeight;
}

onMounted(() => window.addEventListener('resize', handleResize));
onBeforeUnmount(() => window.removeEventListener('resize', handleResize));

return state;
}

6. 综合实战:将“分页 + 搜索 + 数据获取”的 mixins 组合迁移为 Composables

场景:假设一个组件原先使用了三个 mixins——paginationMixin(分页)、searchMixin(搜索)、fetchMixin(数据获取)。我们将它们迁移为三个独立的 Composable 函数,并在组件中组合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// composables/usePagination.js
import { ref, computed } from 'vue';

export function usePagination(initialPage = 1, pageSize = 10) {
const currentPage = ref(initialPage);
const perPage = ref(pageSize);

const offset = computed(() => (currentPage.value - 1) * perPage.value);

function goToPage(page) {
currentPage.value = page;
}

function nextPage() {
currentPage.value++;
}

function prevPage() {
if (currentPage.value > 1) currentPage.value--;
}

return { currentPage, perPage, offset, goToPage, nextPage, prevPage };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// composables/useSearch.js
import { ref, watch } from 'vue';

export function useSearch(initialQuery = '') {
const query = ref(initialQuery);
const debouncedQuery = ref(initialQuery);

let timer = null;
watch(query, (newQuery) => {
clearTimeout(timer);
timer = setTimeout(() => {
debouncedQuery.value = newQuery;
}, 300);
});

function setQuery(q) {
query.value = q;
}

return { query, debouncedQuery, setQuery };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// composables/useDataFetch.js
import { ref, watch, onMounted } from 'vue';

export function useDataFetch(fetchFn, dependencies = []) {
const data = ref([]);
const loading = ref(false);
const error = ref(null);

async function execute() {
loading.value = true;
error.value = null;
try {
data.value = await fetchFn();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}

// 监听依赖变化,自动重新获取
if (dependencies.length > 0) {
watch(dependencies, execute, { deep: true });
} else {
onMounted(execute);
}

return { data, loading, error, refetch: execute };
}

在组件中组合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script setup>
import { usePagination } from '@/composables/usePagination';
import { useSearch } from '@/composables/useSearch';
import { useDataFetch } from '@/composables/useDataFetch';

// 分页
const { currentPage, perPage, goToPage, nextPage, prevPage } = usePagination(1, 10);

// 搜索
const { debouncedQuery, setQuery } = useSearch('');

// 数据获取:依赖分页和搜索
const { data: items, loading, refetch } = useDataFetch(
() => fetch(`/api/items?q=${debouncedQuery.value}&page=${currentPage.value}&size=${perPage.value}`).then(r => r.json()),
[debouncedQuery, currentPage, perPage]
);
</script>

<template>
<div>
<input v-model="searchInput" @input="setQuery($event.target.value)" placeholder="搜索..." />
<button @click="refetch">刷新</button>
<ul v-if="!loading">
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
<p v-else>加载中...</p>
<div>
<button @click="prevPage" :disabled="currentPage <= 1">上一页</button>
<span>{{ currentPage }}</span>
<button @click="nextPage">下一页</button>
</div>
</div>
</template>

迁移成果

  • 三个 Composables 各自职责清晰,可在任何组件中独立复用。
  • 数据依赖关系通过 useDataFetch 的第二个参数显式声明,消除了隐式耦合。
  • 组件代码变得简洁——仅声明 Composable 的调用和渲染逻辑。
  • 任何 Composable 的修改不会意外影响其他功能模块,作用域完全隔离。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 关于 Vue mixins 的缺点,以下哪项描述是错误的?
    A. 多个 mixins 可能导致命名冲突。
    B. mixin 中的代码来源不透明,难以追踪。
    C. mixins 可以与组合式 API 无缝协作,没有迁移必要。
    D. mixins 可能隐式依赖组件中的数据,造成紧耦合。

  2. (单选) 在将 mixin 迁移到 Composable 时,原先 mixin 中对组件属性的依赖应如何处理?
    A. 继续在 Composable 内部通过 this 访问。
    B. 将该属性作为 Composable 函数的参数传入。
    C. 使用全局事件总线传递。
    D. 直接在 Composable 内部使用 inject 获取。

  3. (填空) 在 Composable 函数中注册的 onMounted 钩子会自动绑定到 ______ 的组件实例上。

  4. (多选) 以下哪些是组合式 API 相比选项式 API 的优势?
    A. 更好的逻辑复用(通过 Composable 函数)。
    B. TypeScript 类型推导更自然。
    C. 代码按功能关注点内聚,而非分散在不同选项中。
    D. 不再需要声明 propsemits

二、AI 编程任务:编写面向 AI 的提示词

场景:你有一个 Vue 2 的 mixin,需要在 Vue 3 项目中迁移为 Composable。该 mixin 实现了防抖输入功能:它提供一个 inputValue data、一个 debouncedValue computed(基于 inputValue 延迟 500ms 更新),以及一个 updateValue 方法。组件中通过 this.inputValuethis.debouncedValue 使用。请将其迁移为 useDebouncedInput Composable 函数,并提供在 Vue 3 <script setup> 中的使用示例。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 Composable 函数代码和使用示例。提示词中需明确指定 refwatchsetTimeout 的用法,以及函数返回的对象结构。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请将一个 Vue 2 的防抖输入 mixin 迁移为 Vue 3 Composable 函数。需要创建文件 src/composables/useDebouncedInput.js

  • 函数签名:export function useDebouncedInput(initialValue = '', delay = 500)
  • 使用 ref 定义 inputValuedebouncedValue,初始值均为 initialValue
  • 使用 watch 监听 inputValue,在回调中设置 setTimeout 延迟 delay 毫秒后更新 debouncedValue.value = inputValue.value。每次 inputValue 变化时清除上一次的定时器。
  • 提供 updateValue(newValue) 方法,直接设置 inputValue.value = newValue
  • 返回 { inputValue, debouncedValue, updateValue }
  • 添加 JSDoc 注释。
  • 在文件末尾的注释中提供一个使用示例(Vue 3 <script setup> 片段),展示如何绑定到 <input> 并显示防抖后的值。
    完成后输出完整文件内容。

四、面试真题与参考答案

题目(百度前端面试题):

请详细对比 Vue 的 mixins 和组合式 API 中的 Composable 函数,说明 mixins 存在哪些痛点,组合式 API 是如何解决的。如果团队中仍有大量基于 mixins 的遗留代码,你会建议怎样的迁移策略?

参考答案

Mixins 的三大痛点:命名冲突——多个 mixins 及组件自身可能定义同名属性或方法,合并策略模糊且不易察觉;来源不透明——无法从组件代码一眼看出某个方法来自哪个 mixin,调试和重构时需要追溯继承链;隐式依赖与紧耦合——mixin 可能依赖组件中的特定属性,这种依赖没有显式声明,组件修改后 mixin 可能静默失效。

组合式 API 的 Composable 函数是显式的:所有依赖通过函数参数传递,返回值通过解构获取。开发者可以清晰地看到每个 Composable 的导入路径和参数列表。没有命名冲突——如果两个 Composable 返回同名的变量,解构时可以重命名。Composable 与组件实例解耦——生命周期钩子(onMounted 等)在 Composable 内部注册,会自动绑定到调用 Composable 的组件上,无需组件关心。

渐进式迁移策略:(1)新功能一律使用 Composable 实现,停止编写新的 mixin;(2)优先重构最复杂、导致最多 Bug 的 mixin;(3)使用适配层(wrapper)包装旧 mixin,对外提供 Composable 接口,新旧代码共存;(4)逐步替换所有 mixin 引用,最终移除 mixin 文件;(5)在迁移过程中保持测试覆盖,确保行为一致。


课后练习答案

一、概念自测答案

  1. C

    • 解析:Mixins 与组合式 API 可以共存,但由于 mixins 存在命名冲突、来源不透明等问题,强烈建议迁移到 Composable。C 说“没有迁移必要”是错误的。
  2. B

    • 解析:迁移时应将隐式依赖转换为显式参数。A 错误(Composable 没有 this),C 不推荐(事件总线增加复杂性),D 可能过度耦合。
  3. 调用该 Composable 的

    • 解析:生命周期钩子在 Composable 内部注册时,会自动绑定到当前执行 setup 的组件实例上。
  4. A、B、C

    • 解析:D 错误,组合式 API 仍需要使用 definePropsdefineEmits 声明 Props 和事件。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请将 Vue 2 的防抖输入 mixin 迁移为 Vue 3 Composable 函数 useDebouncedInput(initialValue, delay)。要求:

  • 使用 ref 定义 inputValuedebouncedValue
  • watch 监听 inputValue,通过 setTimeout 延迟更新 debouncedValue
  • 提供 updateValue 方法。
  • 返回 { inputValue, debouncedValue, updateValue }
  • 在注释中提供 <script setup> 使用示例。输出完整代码。”
CATALOG
  1. 1. 第112课:选项式 API 对比与迁移——代码组织、逻辑复用、mixins 迁移到组合式
    1. 1.1. 1. 选项式 API 与组合式 API 的代码组织对比
      1. 1.1.1. 1.1 选项式 API 的逻辑碎片问题
      2. 1.1.2. 1.2 组合式 API 的功能内聚
      3. 1.1.3. 1.3 两种 API 的全面对比
    2. 1.2. 2. Mixins 的痛点:为什么需要迁移?
      1. 1.2.1. 2.1 命名冲突
      2. 1.2.2. 2.2 来源不透明
      3. 1.2.3. 2.3 隐式依赖与紧耦合
    3. 1.3. 3. 从 Mixins 迁移到 Composable:完整步骤
      1. 1.3.1. 3.1 案例:一个“窗口尺寸监听” mixin 的迁移
      2. 1.3.2. 3.2 处理依赖:从隐式 this 到显式参数
      3. 1.3.3. 3.3 处理生命周期钩子:直接在 Composable 中注册
    4. 1.4. 4. 多个 Mixins 到多个 Composables 的协作
      1. 1.4.1. 4.1 避免过度解构导致响应式丢失
    5. 1.5. 5. 混合项目中的渐进式迁移策略
    6. 1.6. 6. 综合实战:将“分页 + 搜索 + 数据获取”的 mixins 组合迁移为 Composables
    7. 1.7. 课后练习
      1. 1.7.1. 一、概念自测(选择题 / 填空题)
      2. 1.7.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.7.3. 三、Agent 模式下的提示词示例
      4. 1.7.4. 四、面试真题与参考答案
    8. 1.8. 课后练习答案
      1. 1.8.1. 一、概念自测答案
      2. 1.8.2. 二、AI 编程任务参考答案(提示词示例)