feat: Add Vue3 exercises and interview plan

- Introduced Vue3 exercises covering composable API, reactivity, lifecycle hooks, and built-in components.
- Added structured interview plan for frontend candidates focusing on HTML, CSS, JavaScript, TypeScript, and Vue.
- Included starter files for each exercise and detailed README documentation for guidance.
This commit is contained in:
charlie
2026-03-24 23:02:58 +08:00
parent 3435848495
commit d0d8be443b
41 changed files with 1551 additions and 5 deletions

View File

@@ -0,0 +1,23 @@
# 练习 1createApp、setup 和 ref
## 目标
学会用 Vue3 的 `createApp``setup()` 启动页面,并用 `ref` 创建最基础的响应式数据。
## 你要练什么
- `createApp`
- `setup()`
- `ref`
- 模板插值
## 任务
- 显示页面标题和学习人数
- 点击按钮后让人数加 1
- 在控制台输出最新人数
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/01-create-app-and-ref/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/01-create-app-and-ref/starter.js)

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>createApp、setup 和 ref</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f5f7fb; }
.panel { max-width: 720px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dce5f2; }
button { padding: 10px 16px; border: 0; border-radius: 999px; background: #2d6cdf; color: #fff; cursor: pointer; }
</style>
</head>
<body>
<section id="app" class="panel">
<h1>{{ title }}</h1>
<p>当前学习人数:{{ learnerCount }}</p>
<button type="button" @click="increaseLearner">加入学习</button>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,20 @@
const { createApp, ref } = Vue;
createApp({
setup() {
const title = ref("Vue3 基础入门");
const learnerCount = ref(16);
function increaseLearner() {
// 任务:
// 1. learnerCount 加 1
// 2. 在控制台输出最新人数
}
return {
title,
learnerCount,
increaseLearner,
};
},
}).mount("#app");

View File

@@ -0,0 +1,23 @@
# 练习 2reactive 和 computed
## 目标
学会把一组相关数据放进 `reactive`,并用 `computed` 推导派生结果。
## 你要练什么
- `reactive`
- `computed`
- 响应式对象
- 派生状态
## 任务
-`reactive` 管理课程信息
-`computed` 计算进度文案
- 点击按钮后让完成课时增加
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/02-reactive-and-computed/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/02-reactive-and-computed/starter.js)

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>reactive 和 computed</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f6f8fc; }
.panel { max-width: 720px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dbe4f1; }
button { padding: 10px 16px; border: 0; border-radius: 999px; background: #1c2f52; color: #fff; cursor: pointer; }
</style>
</head>
<body>
<section id="app" class="panel">
<h1>{{ course.title }}</h1>
<p>总课时:{{ course.totalLessons }}</p>
<p>已完成:{{ course.finishedLessons }}</p>
<p>{{ progressText }}</p>
<button type="button" @click="finishOneLesson">完成一节</button>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,26 @@
const { createApp, reactive, computed } = Vue;
createApp({
setup() {
const course = reactive({
title: "Vue3 响应式基础",
totalLessons: 10,
finishedLessons: 3,
});
const progressText = computed(() => {
// 任务:返回类似 “当前已完成 3 / 10 节”
return "";
});
function finishOneLesson() {
// 任务在不超过总课时的前提下finishedLessons 加 1
}
return {
course,
progressText,
finishOneLesson,
};
},
}).mount("#app");

View File

@@ -0,0 +1,23 @@
# 练习 3watch 和 watchEffect
## 目标
学会区分“监听指定数据变化”和“自动收集依赖并执行副作用”。
## 你要练什么
- `watch`
- `watchEffect`
- 搜索关键字监听
- 副作用日志
## 任务
- 输入关键字时,用 `watch` 输出变化日志
-`watchEffect` 输出当前筛选信息
- 根据关键字过滤课程列表
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/03-watch-and-watch-effect/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/03-watch-and-watch-effect/starter.js)

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>watch 和 watchEffect</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f7f9fd; }
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #d9e2f0; }
input { width: 100%; padding: 12px; border-radius: 12px; border: 1px solid #ccd7e9; }
</style>
</head>
<body>
<section id="app" class="panel">
<h1>课程搜索</h1>
<input v-model="keyword" type="text" placeholder="输入关键字" />
<ul>
<li v-for="item in filteredCourses" :key="item.id">{{ item.title }}</li>
</ul>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,30 @@
const { createApp, ref, computed, watch, watchEffect } = Vue;
createApp({
setup() {
const keyword = ref("");
const courses = ref([
{ id: 1, title: "ref 和 reactive" },
{ id: 2, title: "watch 和 watchEffect" },
{ id: 3, title: "组件通信" },
]);
const filteredCourses = computed(() => {
// 任务:根据 keyword 过滤课程
return courses.value;
});
watch(keyword, (newValue, oldValue) => {
// 任务:输出关键字变化日志
});
watchEffect(() => {
// 任务:输出当前筛选后的数量
});
return {
keyword,
filteredCourses,
};
},
}).mount("#app");

View File

@@ -0,0 +1,23 @@
# 练习 4生命周期和模板 ref
## 目标
学会在组合式 API 里使用生命周期钩子,并通过模板 `ref` 获取 DOM。
## 你要练什么
- `onMounted`
- `onUpdated`
- `onUnmounted`
- 模板 `ref`
## 任务
- 页面挂载后自动聚焦输入框
- 数据更新后输出日志
- 页面卸载前清理定时器
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/04-lifecycle-and-template-ref/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/04-lifecycle-and-template-ref/starter.js)

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>生命周期和模板 ref</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f4f8fb; }
.panel { max-width: 720px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #d8e4f3; }
input { width: 100%; padding: 12px; border-radius: 12px; border: 1px solid #cad7ea; }
</style>
</head>
<body>
<section id="app" class="panel">
<h1>生命周期练习</h1>
<input ref="keywordInput" v-model="keyword" type="text" placeholder="页面挂载后请自动聚焦" />
<p>当前输入:{{ keyword }}</p>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
const { createApp, ref, onMounted, onUpdated, onUnmounted } = Vue;
createApp({
setup() {
const keyword = ref("");
const keywordInput = ref(null);
let timer = null;
onMounted(() => {
// 任务:
// 1. 让输入框自动聚焦
// 2. 建立一个定时器
});
onUpdated(() => {
// 任务:输出 updated 日志
});
onUnmounted(() => {
// 任务:清理定时器并输出销毁日志
});
return {
keyword,
keywordInput,
};
},
}).mount("#app");

View File

@@ -0,0 +1,22 @@
# 练习 5props 和 emit
## 目标
学会在 Vue3 里写基础组件通信。
## 你要练什么
- `props`
- `emit`
- 组件拆分
## 任务
- 把课程项拆成子组件
- 父组件传入课程对象
- 子组件点击按钮后通知父组件切换完成状态
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/05-props-and-emits/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/05-props-and-emits/starter.js)

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>props 和 emit</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f4f7fb; }
.wrap { max-width: 820px; margin: 0 auto; display: grid; gap: 14px; }
.card { padding: 18px; border-radius: 16px; background: #fff; border: 1px solid #d9e3f2; }
.done { color: #1f8f54; }
button { padding: 10px 14px; border: 0; border-radius: 999px; background: #2d6cdf; color: #fff; cursor: pointer; }
</style>
</head>
<body>
<section id="app" class="wrap">
<course-item
v-for="course in courses"
:key="course.id"
:course="course"
@toggle="toggleCourse"
></course-item>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,39 @@
const { createApp } = Vue;
createApp({
components: {
CourseItem: {
props: {
course: {
type: Object,
required: true,
},
},
emits: ["toggle"],
template: `
<article class="card">
<h2>{{ course.title }}</h2>
<p :class="{ done: course.finished }">
{{ course.finished ? "已完成" : "学习中" }}
</p>
<button type="button" @click="$emit('toggle', course.id)">切换状态</button>
</article>
`,
},
},
setup() {
const courses = Vue.ref([
{ id: 1, title: "组合式 API", finished: true },
{ id: 2, title: "组件通信", finished: false },
]);
function toggleCourse(courseId) {
// 任务:根据 courseId 切换对应课程的 finished
}
return {
courses,
toggleCourse,
};
},
}).mount("#app");

View File

@@ -0,0 +1,22 @@
# 练习 6slot 与 provide / inject
## 目标
学会更灵活地组织组件树中的内容和共享信息。
## 你要练什么
- `slot`
- `provide`
- `inject`
## 任务
- 用插槽自定义卡片按钮文案
- 父组件通过 `provide` 提供主题色
- 子组件通过 `inject` 使用主题信息
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/06-slots-and-provide-inject/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/06-slots-and-provide-inject/starter.js)

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>slot 与 provide/inject</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f6f8fc; }
.wrap { max-width: 820px; margin: 0 auto; }
.card { padding: 20px; border-radius: 18px; background: #fff; border: 1px solid #dce5f2; }
button { padding: 10px 14px; border: 0; border-radius: 999px; color: #fff; cursor: pointer; }
</style>
</head>
<body>
<section id="app" class="wrap">
<theme-card>
<template #default>
继续学习
</template>
</theme-card>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
const { createApp, provide, inject } = Vue;
createApp({
components: {
ThemeCard: {
setup() {
const themeColor = inject("themeColor");
return {
themeColor,
};
},
template: `
<article class="card">
<h2>主题卡片</h2>
<button type="button" :style="{ background: themeColor }">
<slot>默认按钮</slot>
</button>
</article>
`,
},
},
setup() {
// 任务:通过 provide 提供 themeColor
return {};
},
}).mount("#app");

View File

@@ -0,0 +1,26 @@
# 练习 7composable 和异步状态
## 目标
学会把可复用逻辑抽成 composable并管理 loading / error / data。
## 你要练什么
- composable
- `ref`
- 异步状态
- `loading`
- `error`
## 任务
- 把课程请求逻辑抽成 `useCourses`
- 页面加载时调用它
- 显示 loading
- 请求成功后渲染列表
- 请求失败时显示错误信息
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/07-composable-and-async/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/07-composable-and-async/starter.js)

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>composable 和异步状态</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f5f8fc; }
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dbe4f2; }
.error { color: #b42318; }
</style>
</head>
<body>
<section id="app" class="panel">
<h1>课程请求练习</h1>
<p v-if="loading">数据加载中...</p>
<p v-if="error" class="error">{{ error }}</p>
<ul v-if="!loading && !error">
<li v-for="item in courses" :key="item.id">{{ item.title }}</li>
</ul>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,38 @@
const { createApp, ref, onMounted } = Vue;
function useCourses() {
const courses = ref([]);
const loading = ref(true);
const error = ref("");
async function loadCourses() {
// 任务:
// 1. 模拟异步请求
// 2. 成功时给 courses 赋值
// 3. 失败时给 error 赋值
// 4. 最后把 loading 设为 false
}
return {
courses,
loading,
error,
loadCourses,
};
}
createApp({
setup() {
const { courses, loading, error, loadCourses } = useCourses();
onMounted(() => {
// 任务:页面挂载后调用 loadCourses
});
return {
courses,
loading,
error,
};
},
}).mount("#app");

View File

@@ -0,0 +1,30 @@
# 练习 8Vue3 综合小面板
## 目标
把 Vue3 组合式 API 的主线能力串起来,完成一个小型课程面板。
## 你要练什么
- `ref`
- `reactive`
- `computed`
- `watch`
- 组件通信
- composable
- 模板 `ref`
## 任务
- 做一个课程搜索和筛选面板
-`computed` 计算筛选结果和统计数据
-`watch` 输出搜索关键字变化
- 用子组件渲染课程卡片
- 点击按钮切换课程完成状态
- 点击按钮聚焦搜索框
- 抽一个 composable 管理课程数据
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/08-final-dashboard/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/08-final-dashboard/starter.js)

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue3 综合小面板</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f4f7fb; }
.page { max-width: 960px; margin: 0 auto; display: grid; gap: 18px; }
.panel { padding: 22px; border-radius: 20px; background: #fff; border: 1px solid #d9e4f1; }
.toolbar { display: grid; grid-template-columns: 1fr 180px 160px; gap: 12px; }
input, select, button { padding: 12px 14px; border-radius: 12px; border: 1px solid #cad6e8; font: inherit; }
button { border: 0; background: #2d6cdf; color: #fff; cursor: pointer; }
.list { display: grid; gap: 14px; }
.course-card { padding: 18px; border-radius: 16px; border: 1px solid #dbe4f1; background: #fbfcfe; }
.course-card.is-done { border-color: #6cc18a; background: #f3fbf6; }
</style>
</head>
<body>
<section id="app" class="page">
<article class="panel">
<h1>Vue3 综合课程面板</h1>
<div class="toolbar">
<input ref="searchInput" v-model="keyword" type="text" placeholder="输入课程关键字" />
<select v-model="statusFilter">
<option value="all">全部</option>
<option value="done">已完成</option>
<option value="doing">学习中</option>
</select>
<button type="button" @click="focusSearch">聚焦搜索框</button>
</div>
</article>
<section class="panel">
<div v-if="!filteredCourses.length">暂无匹配结果</div>
<div v-show="filteredCourses.length" class="list">
<course-item
v-for="course in filteredCourses"
:key="course.id"
:course="course"
@toggle="toggleCourse"
></course-item>
</div>
</section>
<footer class="panel">
<p>总课程数:{{ totalCount }}</p>
<p>已完成数:{{ finishedCount }}</p>
<p>当前筛选数:{{ visibleCount }}</p>
</footer>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,85 @@
const { createApp, ref, computed, watch } = Vue;
function useCourses() {
const courses = ref([
{ id: 1, title: "setup 和 ref", finished: true },
{ id: 2, title: "reactive 和 computed", finished: false },
{ id: 3, title: "组件通信", finished: false },
{ id: 4, title: "composable 实战", finished: true },
]);
function toggleCourse(courseId) {
// 任务:根据 courseId 切换课程完成状态
}
return {
courses,
toggleCourse,
};
}
createApp({
components: {
CourseItem: {
props: {
course: {
type: Object,
required: true,
},
},
emits: ["toggle"],
template: `
<article class="course-card" :class="{ 'is-done': course.finished }">
<h2>{{ course.title }}</h2>
<p>{{ course.finished ? "已完成" : "学习中" }}</p>
<button type="button" @click="$emit('toggle', course.id)">切换状态</button>
</article>
`,
},
},
setup() {
const keyword = ref("");
const statusFilter = ref("all");
const searchInput = ref(null);
const { courses, toggleCourse } = useCourses();
const filteredCourses = computed(() => {
// 任务:同时按关键字和状态筛选课程
return courses.value;
});
const totalCount = computed(() => {
return courses.value.length;
});
const finishedCount = computed(() => {
// 任务:返回已完成数量
return 0;
});
const visibleCount = computed(() => {
// 任务:返回当前筛选后的数量
return 0;
});
watch(keyword, (newValue) => {
// 任务:输出关键字变化
});
function focusSearch() {
// 任务:通过模板 ref 聚焦输入框
}
return {
keyword,
statusFilter,
searchInput,
filteredCourses,
totalCount,
finishedCount,
visibleCount,
toggleCourse,
focusSearch,
};
},
}).mount("#app");

View File

@@ -0,0 +1,24 @@
# 练习 9toRef、toRefs 和 readonly
## 目标
学会把响应式对象里的字段拆出来继续保持响应式,并理解只读数据的使用场景。
## 你要练什么
- `toRef`
- `toRefs`
- `readonly`
## 任务
-`reactive` 管理学习者信息
-`toRef` 单独取出 `name`
-`toRefs` 拆出其余字段
-`readonly` 包一层设置项
- 点击按钮更新学习者信息并观察页面变化
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/09-reactivity-helpers/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/09-reactivity-helpers/starter.js)

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>toRef、toRefs 和 readonly</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f5f8fc; }
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dbe4f2; }
button { padding: 10px 16px; border: 0; border-radius: 999px; background: #2d6cdf; color: #fff; cursor: pointer; }
code { background: #f2f6fb; padding: 2px 6px; border-radius: 8px; }
</style>
</head>
<body>
<section id="app" class="panel">
<h1>{{ name }}</h1>
<p>当前阶段:{{ stage }}</p>
<p>学习天数:{{ studyDays }}</p>
<p>主题模式:<code>{{ settings.theme }}</code></p>
<button type="button" @click="updateProfile">更新资料</button>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,33 @@
const { createApp, reactive, toRef, toRefs, readonly } = Vue;
createApp({
setup() {
const profile = reactive({
name: "林晨",
stage: "Vue3 入门",
studyDays: 12,
});
const name = toRef(profile, "name");
const { stage, studyDays } = toRefs(profile);
const settings = readonly({
theme: "light",
});
function updateProfile() {
// 任务:
// 1. 更新 name.value
// 2. 更新 stage.value
// 3. 让 studyDays.value + 1
// 4. 不要直接修改 settings.theme
}
return {
name,
stage,
studyDays,
settings,
updateProfile,
};
},
}).mount("#app");

View File

@@ -0,0 +1,24 @@
# 练习 10nextTick 和组件 v-model
## 目标
学会在 DOM 更新完成后执行逻辑,并理解 Vue3 组件 `v-model` 的通信约定。
## 你要练什么
- `nextTick`
- 组件 `v-model`
- `modelValue`
- `update:modelValue`
## 任务
- 封装一个搜索输入子组件
- 父组件通过 `v-model` 绑定关键字
- 点击“展开搜索区”后,等 DOM 更新完成再聚焦输入框
- 在控制台输出关键字变化
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/10-next-tick-and-component-v-model/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/10-next-tick-and-component-v-model/starter.js)

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>nextTick 和组件 v-model</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f4f7fb; }
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #d9e4f1; }
input, button { padding: 12px 14px; border-radius: 12px; font: inherit; }
input { width: 100%; border: 1px solid #cad6e8; }
button { border: 0; background: #2d6cdf; color: #fff; cursor: pointer; margin-bottom: 16px; }
</style>
</head>
<body>
<section id="app" class="panel">
<button type="button" @click="toggleSearch">展开搜索区</button>
<div v-if="showSearch">
<search-input ref="searchBox" v-model="keyword"></search-input>
</div>
<p>当前关键字:{{ keyword }}</p>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,52 @@
const { createApp, ref, nextTick, watch } = Vue;
createApp({
components: {
SearchInput: {
props: {
modelValue: {
type: String,
default: "",
},
},
emits: ["update:modelValue"],
template: `
<input
ref="inputEl"
:value="modelValue"
type="text"
placeholder="请输入课程关键字"
@input="$emit('update:modelValue', $event.target.value)"
/>
`,
methods: {
focus() {
this.$refs.inputEl.focus();
},
},
},
},
setup() {
const showSearch = ref(false);
const keyword = ref("");
const searchBox = ref(null);
watch(keyword, (newValue) => {
// 任务:在控制台输出关键字变化
});
async function toggleSearch() {
// 任务:
// 1. 切换 showSearch.value
// 2. 如果展开了await nextTick()
// 3. 通过 searchBox.value.focus() 聚焦输入框
}
return {
showSearch,
keyword,
searchBox,
toggleSearch,
};
},
}).mount("#app");

View File

@@ -0,0 +1,24 @@
# 练习 11before 系列生命周期和 expose
## 目标
学会在组合式 API 中使用 before 系列生命周期,并理解子组件如何有选择地暴露能力给父组件。
## 你要练什么
- `onBeforeMount`
- `onBeforeUpdate`
- `onBeforeUnmount`
- `expose`
## 任务
- 在不同生命周期里输出日志
- 父组件通过模板 `ref` 获取子组件实例
- 子组件通过 `expose` 暴露一个 `focusInput` 方法
- 父组件点击按钮后调用这个暴露出来的方法
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/11-before-hooks-and-expose/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/11-before-hooks-and-expose/starter.js)

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>before 系列生命周期和 expose</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f6f8fc; }
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dde5f2; }
button, input { padding: 12px 14px; border-radius: 12px; font: inherit; }
input { width: 100%; border: 1px solid #ccd7e9; margin-top: 12px; }
button { border: 0; background: #2d6cdf; color: #fff; cursor: pointer; margin-right: 10px; }
</style>
</head>
<body>
<section id="app" class="panel">
<button type="button" @click="focusChildInput">聚焦子组件输入框</button>
<button type="button" @click="showChild = !showChild">切换子组件显示</button>
<child-panel v-if="showChild" ref="childPanel"></child-panel>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,59 @@
const {
createApp,
ref,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount,
} = Vue;
createApp({
components: {
ChildPanel: {
template: `
<div>
<p>我是子组件</p>
<input ref="inputEl" type="text" placeholder="等待父组件调用 focus" />
</div>
`,
setup(props, { expose }) {
const inputEl = ref(null);
onBeforeMount(() => {
// 任务:输出 beforeMount 日志
});
onBeforeUpdate(() => {
// 任务:输出 beforeUpdate 日志
});
onBeforeUnmount(() => {
// 任务:输出 beforeUnmount 日志
});
function focusInput() {
// 任务:聚焦 inputEl
}
// 任务:通过 expose 暴露 focusInput
return {
inputEl,
};
},
},
},
setup() {
const showChild = ref(true);
const childPanel = ref(null);
function focusChildInput() {
// 任务:调用 childPanel.value 暴露出来的方法
}
return {
showChild,
childPanel,
focusChildInput,
};
},
}).mount("#app");

View File

@@ -0,0 +1,22 @@
# 练习 12Teleport、Suspense 和 Transition
## 目标
认识 Vue3 常见内置组件在实际页面里的使用方式。
## 你要练什么
- `Teleport`
- `Suspense`
- `Transition`
## 任务
-`Teleport` 把弹层渲染到 `body`
-`Transition` 给弹层或提示做显隐动画
-`Suspense` 包裹一个异步组件,并显示 fallback
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/12-built-in-components/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/12-built-in-components/starter.js)

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Teleport、Suspense 和 Transition</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f4f7fb; }
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #d9e4f1; }
.modal { position: fixed; inset: 0; display: grid; place-items: center; background: rgba(12, 17, 29, 0.45); }
.modal-card { width: min(420px, calc(100vw - 32px)); padding: 24px; border-radius: 18px; background: #fff; }
.fade-enter-active, .fade-leave-active { transition: opacity .24s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
button { padding: 10px 14px; border-radius: 12px; border: 0; background: #2d6cdf; color: #fff; cursor: pointer; }
</style>
</head>
<body>
<section id="app" class="panel">
<h1>内置组件练习</h1>
<button type="button" @click="showModal = !showModal">切换弹层</button>
<Suspense>
<template #default>
<async-info></async-info>
</template>
<template #fallback>
<p>异步组件加载中...</p>
</template>
</Suspense>
<Teleport to="body">
<Transition name="fade">
<div v-if="showModal" class="modal">
<div class="modal-card">
<h2>练习弹层</h2>
<p>这里应该通过 Teleport 渲染到 body。</p>
<button type="button" @click="showModal = false">关闭</button>
</div>
</div>
</Transition>
</Teleport>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,24 @@
const { createApp, ref } = Vue;
createApp({
components: {
AsyncInfo: {
async setup() {
// 任务:
// 1. 模拟等待
// 2. 返回需要在模板中展示的数据
return {};
},
template: `
<p>这里会展示异步组件内容。</p>
`,
},
},
setup() {
const showModal = ref(false);
return {
showModal,
};
},
}).mount("#app");

View File

@@ -0,0 +1,26 @@
# 练习 13script setup、defineProps、defineEmits、defineExpose
## 目标
补上 Vue3 在工程化单文件组件里的核心宏语法。
## 你要练什么
- `<script setup>`
- `defineProps`
- `defineEmits`
- `defineExpose`
## 说明
这一题不是浏览器 CDN 练习,而是单文件组件语法练习,需要放在 `Vite + Vue3` 之类的工程里使用。
## 任务
- 给子组件定义 `title``finished` 两个 props
- 定义 `toggle` 事件并在按钮点击时触发
- 暴露一个 `focusAction` 方法给父组件调用
## 文件
- [starter.vue](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/13-script-setup-macros/starter.vue)

View File

@@ -0,0 +1,38 @@
<template>
<article class="course-card">
<h2>{{ title }}</h2>
<p>{{ finished ? "已完成" : "学习中" }}</p>
<button ref="actionButton" type="button" @click="handleToggle">
切换状态
</button>
</article>
</template>
<script setup>
import { ref } from "vue";
// 任务:
// 1. 用 defineProps 定义 title 和 finished
// 2. 用 defineEmits 定义 toggle
// 3. 点击按钮时触发 toggle
// 4. 用 defineExpose 暴露 focusAction
const actionButton = ref(null);
function handleToggle() {
// 在这里触发 emit
}
function focusAction() {
actionButton.value?.focus();
}
</script>
<style scoped>
.course-card {
padding: 18px;
border-radius: 16px;
border: 1px solid #dbe4f1;
background: #fbfcfe;
}
</style>

141
08-vue3/README.md Normal file
View File

@@ -0,0 +1,141 @@
# Vue3组合式 API + 响应式原理)
你的学习文档里这一章的定位是 `Vue3组合式API + 响应式原理)`。这一章我按这个方向来拆,不再重复 Vue2 的 Options API 主线,而是重点转到 Vue3 的组合式写法和响应式思维。
## 学完后你应该掌握
- Vue3 和 Vue2 的核心差异
- `createApp`
- `setup()`
- `ref`
- `reactive`
- `toRef`
- `toRefs`
- `readonly`
- `computed`
- `watch`
- `watchEffect`
- `nextTick`
- 组合式 API 生命周期
- 模板 `ref`
- `props``emit`
- 组件 `v-model`
- `slot`
- `provide` / `inject`
- `expose`
- composable 的基本抽离方式
- `script setup`
- `defineProps`
- `defineEmits`
- `defineExpose`
- `Teleport`
- `Suspense`
- `Transition`
- 如何用 Vue3 写一个小型管理面板
## 这一章在解决什么
Vue2 更强调“选项式组织”。
Vue3 这一章要解决的是:
- 逻辑如何按功能组织,而不是按选项分散
- 响应式数据如何在 `setup()` 里组合
- 一段可复用逻辑如何抽成 composable
- 组件之间如何在组合式 API 下继续通信
## 全部知识点清单
### 基础入口
- `createApp`
- `setup()`
- `return`
### 响应式核心
- `ref`
- `reactive`
- `toRef`
- `toRefs`
- `readonly`
- `computed`
- `watch`
- `watchEffect`
- `nextTick`
### 生命周期与 DOM
- `onBeforeMount`
- `onMounted`
- `onBeforeUpdate`
- `onUpdated`
- `onBeforeUnmount`
- `onUnmounted`
- 模板 `ref`
- `expose`
### 组件通信
- `props`
- `emit`
- 组件 `v-model`
- `slot`
- `provide`
- `inject`
### 逻辑复用
- composable
- 异步状态管理
- `loading`
- `error`
### 工程化语法与内置组件
- `script setup`
- `defineProps`
- `defineEmits`
- `defineExpose`
- `Teleport`
- `Suspense`
- `Transition`
## 学习顺序
1. `createApp``setup()``ref`
2. `reactive``computed`
3. `watch``watchEffect`
4. 生命周期和模板 `ref`
5. `props``emit`
6. `slot``provide` / `inject`
7. composable 与异步状态
8. `toRefs``readonly` 等响应式辅助工具
9. `nextTick` 和组件 `v-model`
10. before 系列生命周期与 `expose`
11. `Teleport``Suspense``Transition`
12. `script setup`
13. 综合小页面
## 练习目录
- [01-create-app-and-ref/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/01-create-app-and-ref/README.md)
- [02-reactive-and-computed/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/02-reactive-and-computed/README.md)
- [03-watch-and-watch-effect/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/03-watch-and-watch-effect/README.md)
- [04-lifecycle-and-template-ref/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/04-lifecycle-and-template-ref/README.md)
- [05-props-and-emits/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/05-props-and-emits/README.md)
- [06-slots-and-provide-inject/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/06-slots-and-provide-inject/README.md)
- [07-composable-and-async/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/07-composable-and-async/README.md)
- [08-final-dashboard/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/08-final-dashboard/README.md)
- [09-reactivity-helpers/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/09-reactivity-helpers/README.md)
- [10-next-tick-and-component-v-model/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/10-next-tick-and-component-v-model/README.md)
- [11-before-hooks-and-expose/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/11-before-hooks-and-expose/README.md)
- [12-built-in-components/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/12-built-in-components/README.md)
- [13-script-setup-macros/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/13-script-setup-macros/README.md)
## 说明
- 这一章只提供 `starter`,不提供 `answer`
- 为了降低门槛,练习使用浏览器 CDN 版本的 Vue3
- `13-script-setup-macros` 是 SFC 语法练习,文件是 `.vue` starter不是直接双击运行的 HTML
- 如果后面你要把这一章升级成 `Vite + Vue3`,我可以再继续补工程化版本

217
09-interview-plan/README.md Normal file
View File

@@ -0,0 +1,217 @@
# 前端面试官 Prompt
你现在扮演一位有经验的前端面试官,负责基于这个学习仓库的内容,对候选人进行系统化面试训练。
## 你的任务
围绕下面 8 个知识单元出题、追问、点评,并帮助候选人把“会写”升级成“会讲”:
- HTML
- CSS
- JavaScript Core
- DOM + 事件 + 异步
- ES6+
- TypeScript
- Vue2
- Vue3
## 你的工作方式
### 1. 按阶段出题
请按下面 4 个阶段组织面试题:
#### 第一阶段:结构与样式
- HTML
- CSS
目标:
- 检查候选人是否能把页面拆成结构
- 检查候选人是否能解释常见布局方案
#### 第二阶段JavaScript 主线
- JavaScript Core
- DOM + 事件 + 异步
- ES6+
目标:
- 检查候选人是否能说清基础语法、作用域、闭包、`this`
- 检查候选人是否能说清事件流、异步、模块化
#### 第三阶段:类型与 Vue2
- TypeScript
- Vue2
目标:
- 检查候选人是否能解释类型系统的价值
- 检查候选人是否能说清 Vue2 的数据驱动和组件通信
#### 第四阶段Vue3 与综合表达
- Vue3
目标:
- 检查候选人是否能说清组合式 API、响应式、composable 和工程化语法
### 2. 每次只出一个单元
每次面试时:
1. 先让我选择一个单元
2. 再连续提出 5 到 8 道问题
3. 每题都允许我回答
4. 你根据我的回答继续追问
### 3. 每道题都按这个顺序处理
对每道题,请按下面流程进行:
1. 先提出问题
2. 等我回答
3. 判断回答是否完整
4. 如果不完整,继续追问
5. 最后给出点评
## 你的提问标准
每道题尽量围绕这 4 层来设计:
1. 定义是什么
2. 使用场景是什么
3. 常见坑点是什么
4. 能不能举一个小例子
## 题目池
### HTML
- 什么是语义化标签?为什么不要只用 `div`
- 一个完整 HTML 文档的基本骨架包含哪些部分?
- 块级元素和行内元素的区别是什么?
- `section``article``aside` 分别适合什么场景?
- `ul` / `ol` / `li` 的嵌套规则是什么?
- `form``label``input` 的正确关系是什么?
- `alt``href``src``name` 分别有什么作用?
- 为什么说 HTML 更像页面骨架而不是样式代码?
### CSS
- 盒模型由哪些部分组成?
- `margin``padding` 的区别是什么?
- `display: block / inline / inline-block / flex / grid` 的常见差异是什么?
- Flex 最常用的几个属性分别解决什么问题?
- Grid 适合什么场景?和 Flex 的区别是什么?
- `position: relative / absolute / fixed / sticky` 分别怎么理解?
- 什么是文档流?脱离文档流会带来什么影响?
- 如何做水平垂直居中?
### JavaScript Core
- `var``let``const` 的区别是什么?
- JavaScript 常见数据类型有哪些?
- `undefined``null` 的区别是什么?
- `if / else``switch` 各适合什么场景?
- `for``while` 的区别是什么?
- 什么是函数?参数和返回值怎么理解?
- 数组和对象分别适合存什么数据?
- 什么是作用域?什么是闭包?
- `this` 在普通函数、对象方法、箭头函数里的区别是什么?
- 值传递和引用传递怎么理解?
### DOM + 事件 + 异步
- 如何选中页面元素?
- `textContent``innerHTML``classList``style` 有什么常见用途?
- 如何创建、插入、删除节点?
- `addEventListener` 的作用是什么?
- 事件冒泡是什么?事件委托为什么有用?
- `preventDefault()``stopPropagation()` 分别解决什么问题?
- `setTimeout` 为什么体现异步?
- Promise 和 `async/await` 在页面交互里怎么配合?
### ES6+
- 模板字符串和字符串拼接相比有什么优势?
- 解构赋值的典型使用场景是什么?
- 展开运算符和剩余参数分别做什么?
- 箭头函数和普通函数的差异有哪些?
- 为什么箭头函数里的 `this` 容易被问到?
- `import / export` 的基本写法是什么?
- `fetch()``res.json()` 的关系是什么?
- Promise 和 `async/await` 的关系是什么?
### TypeScript
- TypeScript 和 JavaScript 的核心差异是什么?
- 为什么说 TypeScript 的价值主要发生在运行前?
- 基本类型、数组类型、函数类型怎么写?
- `interface` 适合解决什么问题?
- 泛型 `<T>` 的核心价值是什么?
- 联合类型和可选属性的常见场景是什么?
- TypeScript 报错时应该先看什么?
- 为什么说类型要服务于业务?
### Vue2
- Vue2 为什么说是数据驱动视图?
- `new Vue()` 里最常见的几个选项是什么?
- `v-bind``v-on``v-model` 分别做什么?
- `v-if``v-show` 的区别是什么?
- `computed``watch` 的区别是什么?
- Vue2 的生命周期最常问哪几个?
- 父子组件怎么通过 `props``$emit` 通信?
- `slot``ref` 分别适合什么场景?
### Vue3
- Vue3 和 Vue2 最大的思维差异是什么?
- `setup()` 为什么是组合式 API 的入口?
- `ref``reactive` 的区别是什么?
- `watch``watchEffect` 的区别是什么?
- `toRef``toRefs``readonly` 解决什么问题?
- `nextTick` 适合什么场景?
- Vue3 组件 `v-model` 的底层约定是什么?
- `provide / inject` 适合什么场景?
- 什么是 composable
- `script setup``defineProps``defineEmits` 有什么作用?
- `Teleport``Suspense``Transition` 分别用来解决什么问题?
## 回答评估标准
当我回答后,请从这 4 个维度给出判断:
- 是否说对定义
- 是否说出使用场景
- 是否提到常见坑点
- 是否给出例子或代码思路
如果回答不完整,请继续追问,不要立刻公布标准答案。
## 点评格式
每道题点评时请使用这个格式:
```md
问题:...
评价:回答完整 / 基本正确 / 不完整 / 有明显错误
缺失点:...
标准表达:...
追问题:...
```
## 启动方式
当我说“开始面试”时,请先问我:
1. 你想刷哪个单元?
2. 你想要偏基础、偏中等,还是偏追问型?
3. 你想一次刷 5 题还是 8 题?
然后直接开始扮演面试官,不要再解释规则。

View File

@@ -11,6 +11,8 @@
- `05-es6-plus`:预留给 ES6+(现代 JS
- `06-typescript`:预留给 TypeScript
- `07-vue2`:预留给 Vue2
- `08-vue3`:预留给 Vue3
- `09-interview-plan`:前端面试题计划
## 当前可学内容
@@ -64,7 +66,21 @@
- `starter.html` / `starter.js` 起始代码
- `answer.html` / `answer.js` 参考答案
前七部分现在已经补充到“核心主线 + 常见细分知识点”。
现在已经整理好 `08-vue3`,里面包含:
- Vue3组合式 API + 响应式原理)讲义
- 分阶段练习
- `starter.html` / `starter.js` 起始代码
- 当前按你的要求不提供 `answer`
现在也已经整理好 `09-interview-plan`,里面包含:
- 基于 01-08 各单元抽取的核心面试题
- 分阶段复习顺序
- 刷题执行方式
- 复盘目标
前九部分现在都已经补充到“学习主线 + 复习计划 + 面试题整理”。
## 使用方式
@@ -75,9 +91,11 @@
5. 再阅读 [05-es6-plus/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/README.md)
6. 再阅读 [06-typescript/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/README.md)
7. 再阅读 [07-vue2/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/README.md)
8. 按顺序完成每个练习目录
9. 先写 `starter.html``starter.css``starter.js``starter.ts`
10. 写完后再对照答案文件
11. `06-typescript` 时,也可以进入 [06-typescript](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript) 后执行 `npm install``npm run dev`
8. 再阅读 [08-vue3/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/README.md)
9. 再阅读 [09-interview-plan/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/09-interview-plan/README.md)
10. 按顺序完成每个练习目录
11. 先写 `starter.html``starter.css``starter.js``starter.ts`
12. 写完后再对照答案文件
13.`06-typescript` 时,也可以进入 [06-typescript](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript) 后执行 `npm install``npm run dev`
如果你后面要继续学其他知识点,我可以按同样结构继续给你补更多工程化目录。