Week 1-8: Spring Boot 学习计划完整项目
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
226
week6/教案.md
Normal file
226
week6/教案.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# Week 6:Vue 3 前端框架入门
|
||||
|
||||
**目标**:掌握 Vue 3 Composition API + Vite + Vue Router + Pinia,能独立开发 SPA 应用。
|
||||
|
||||
**前置**:已完成 Week 5 Spring Boot 后端,有 HTML/CSS/JS 基础。
|
||||
|
||||
**本周产出**:一个独立的 Todo SPA 应用,包含计数器、动态表单、组件拆分、路由导航、状态管理和 HTTP 封装。
|
||||
|
||||
**启动方式**:
|
||||
```bash
|
||||
cd week6
|
||||
npm install # 首次运行
|
||||
npm run dev # 启动开发服务器 → http://localhost:5173
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Day 1:Vue 3 介绍、Vite 脚手架、组件基础
|
||||
|
||||
**知识点**:
|
||||
- Vue 3 是什么:渐进式前端框架,核心是"响应式数据 + 声明式渲染"
|
||||
- SFC(Single File Component):`.vue` 文件 = `<template>` + `<script setup>` + `<style scoped>`
|
||||
- Composition API vs Options API:为什么选 Composition API
|
||||
- Vite:新一代构建工具,冷启动快(ES Module)、HMR 极速
|
||||
- 项目结构:`main.js`(入口)、`App.vue`(根组件)、`components/`、`views/`
|
||||
|
||||
**动手 → 理解**:
|
||||
1. 打开 `src/main.js`:理解 `createApp(App).mount('#app')` 的过程
|
||||
2. 打开 `src/App.vue`:观察 `<script setup>` 语法糖
|
||||
3. 修改 `index.html` 中的 `<title>`,观察浏览器标签变化
|
||||
4. 在 `HomeView.vue` 的 `<template>` 中加一行 `<p>Hello Vue!</p>`,观察热更新
|
||||
5. 把 `style scoped` 去掉,观察样式是否泄漏到其他组件
|
||||
|
||||
**核心文件**:
|
||||
- `index.html` — 入口 HTML,`<div id="app">` 挂载点
|
||||
- `src/main.js` — 应用初始化
|
||||
- `src/App.vue` — 根组件
|
||||
- `vite.config.js` — Vite 配置(插件、代理、端口)
|
||||
|
||||
**思考题**:`<script setup>` 和普通的 `<script>` 有什么区别?为什么 Vue 3 推荐 setup 语法糖?
|
||||
|
||||
---
|
||||
|
||||
## Day 2:响应式数据(ref/reactive)、计算属性
|
||||
|
||||
**知识点**:
|
||||
- `ref()`:包装基本类型(number、string、boolean),通过 `.value` 访问
|
||||
- `reactive()`:包装对象/数组,直接访问属性(不需要 `.value`)
|
||||
- `computed()`:计算属性,依赖变化时自动重新计算,带缓存
|
||||
- `watch()` / `watchEffect()`:侦听数据变化执行副作用
|
||||
- 响应式的本质:Proxy 拦截 getter/setter,数据变 → 视图自动变
|
||||
- 模板中 ref 自动解包(不需要写 `.value`)
|
||||
|
||||
**动手 → 理解**:
|
||||
1. 打开 `CounterView.vue`,在模板中写 `{{ localCount }}`,不需要 `.value`
|
||||
2. 在 `<script>` 中用 `console.log(localCount.value)` 打印当前值
|
||||
3. 修改 `computed` 的计算逻辑,改为判断"是否大于 10"
|
||||
4. 添加一个 `watch`,在 `localCount` 变化时 `console.log('变了:', newVal)`
|
||||
5. 对比:把 `reactive` 改为 `ref` 写一个对象,体会 `.value` 的区别
|
||||
|
||||
**核心文件**:
|
||||
- `views/CounterView.vue` — ref + computed 演示
|
||||
- `stores/counter.js` — Pinia store(第 6 天预热)
|
||||
|
||||
**思考题**:为什么模板中 ref 自动解包,但 `<script>` 里必须用 `.value`?
|
||||
|
||||
---
|
||||
|
||||
## Day 3:指令系统(v-if/v-for/v-model)
|
||||
|
||||
**知识点**:
|
||||
- `v-if` / `v-else-if` / `v-else`:条件渲染(销毁/重建 DOM)
|
||||
- `v-show`:条件显示(仅切换 `display`)—— 适合频繁切换
|
||||
- `v-for`:列表渲染,必须有 `:key` 唯一标识
|
||||
- `v-bind`(缩写 `:`):动态绑定属性 `:class`、`:style`、`:href`
|
||||
- `v-model`:双向绑定,表单输入 ↔ 数据自动同步
|
||||
- `v-on`(缩写 `@`):事件绑定 `@click`、`@submit`、`@keyup`
|
||||
- `v-model` 修饰符:`.trim`、`.number`、`.lazy`
|
||||
|
||||
**动手 → 理解**:
|
||||
1. 打开 `FormDemoView.vue`,在输入框中输入,观察下方实时预览
|
||||
2. 用 DevTools 检查:勾选"显示详细信息"时,`v-if` 的元素是否在 DOM 中?
|
||||
3. 把 `v-if` 换成 `v-show`,再次检查 DOM 差异
|
||||
4. 在 `v-for` 的 `<li>` 中故意不写 `:key`,观察控制台警告
|
||||
5. 尝试加一个 `v-model.lazy` 的输入框,对比 `input` 事件和 `change` 事件的区别
|
||||
|
||||
**核心文件**:
|
||||
- `views/FormDemoView.vue` — 指令综合演示
|
||||
|
||||
**思考题**:什么时候用 `v-if`,什么时候用 `v-show`?为什么 `v-for` 和 `v-if` 不建议同时使用?
|
||||
|
||||
---
|
||||
|
||||
## Day 4:组件通信(props/emits)、插槽
|
||||
|
||||
**知识点**:
|
||||
- `defineProps()`:父组件向子组件传递数据(单向数据流)
|
||||
- `defineEmits()`:子组件向父组件发送事件(回调模式)
|
||||
- Prop 校验:`type`、`required`、`default`、`validator`
|
||||
- 单向数据流原则:子组件不能直接修改 props
|
||||
- `v-model` 组件版:`modelValue` + `update:modelValue`
|
||||
- `<slot>` 插槽:父组件向子组件传递模板内容
|
||||
- 命名插槽、作用域插槽(高级)
|
||||
|
||||
**动手 → 理解**:
|
||||
1. 打开 `TodoForm.vue` 和 `TodoItem.vue`,对照看 props 和 emits
|
||||
2. 在 `TodoItem` 中尝试直接 `todo.done = true`(不通过 emit),观察 Pinia 是否感知
|
||||
3. 给 `TodoItem` 的 props 加一个 `validator`,限制 `todo.text` 不能为空
|
||||
4. 练习:新建一个 `<Card>` 组件,用 `<slot>` 让父组件填充内容
|
||||
5. 尝试用 `v-model` 模式改写 `TodoForm`(父组件用 v-model 双向绑定输入值)
|
||||
|
||||
**核心文件**:
|
||||
- `components/TodoForm.vue` — props 接收 / emits 发送
|
||||
- `components/TodoItem.vue` — props 接收 / emits 发送
|
||||
- `views/TodoView.vue` — 父组件,组装 TodoForm + TodoItem
|
||||
|
||||
**思考题**:为什么不直接在子组件中操作 Pinia store,而是通过 props/emits 层层传递?
|
||||
|
||||
---
|
||||
|
||||
## Day 5:Vue Router 路由
|
||||
|
||||
**知识点**:
|
||||
- `vue-router`:前端路由,URL 变化时不刷新页面
|
||||
- `createRouter({ history: createWebHistory() })`:HTML5 History 模式
|
||||
- `<router-link>`:导航链接(渲染为 `<a>` 标签)
|
||||
- `<router-view>`:路由出口(匹配的组件渲染在这里)
|
||||
- 路由懒加载:`() => import('../views/XXX.vue')`
|
||||
- `meta`:路由元信息(标题、权限等)
|
||||
- 导航守卫:`beforeEach`、`afterEach`
|
||||
- 动态路由:`/user/:id`
|
||||
|
||||
**动手 → 理解**:
|
||||
1. 点击导航栏各链接,观察 URL 变化和页面内容,按 F12 Network 面板看是否有网络请求
|
||||
2. 在 `router/index.js` 中临时把 `createWebHistory` 改为 `createWebHashHistory`,观察 URL 变化(`#` 号出现)
|
||||
3. 修改某个路由的 `meta.title`,观察浏览器标签变化
|
||||
4. 在导航守卫中加 `console.log('从', from.path, '到', to.path)`
|
||||
5. 添加一个 `/user/:name` 动态路由页面
|
||||
|
||||
**核心文件**:
|
||||
- `router/index.js` — 路由配置 + 导航守卫
|
||||
- `components/NavBar.vue` — `<router-link>` 导航
|
||||
|
||||
**思考题**:为什么前端路由要用 History 模式而不是 Hash 模式?History 模式上线需要注意什么(Nginx 配置)?
|
||||
|
||||
---
|
||||
|
||||
## Day 6:Pinia 状态管理
|
||||
|
||||
**知识点**:
|
||||
- 为什么需要状态管理:跨组件/跨页面共享数据
|
||||
- `defineStore('name', () => { ... })`:Setup Store 语法(与 Composition API 一致)
|
||||
- State:`ref()` / `reactive()` 定义状态
|
||||
- Getters:`computed()` 定义派生状态
|
||||
- Actions:普通函数,可异步
|
||||
- `storeToRefs()`:解构时保持响应性
|
||||
- Store 在 `main.js` 中通过 `app.use(createPinia())` 注入
|
||||
- 组件中直接 `const store = useXxxStore()` 使用
|
||||
|
||||
**动手 → 理解**:
|
||||
1. 打开 `stores/todo.js`,观察 state/getters/actions 的定义方式
|
||||
2. 在浏览器中操作 Todo,打开 Vue DevTools 的 Pinia 面板查看状态变化
|
||||
3. 在 `CounterView.vue` 中对比:Pinia store 的 counter 和本地 ref 的 counter
|
||||
4. 用 `storeToRefs(todoStore)` 解构 `todos`,验证响应性
|
||||
5. 练习:新建一个 `theme` store,管理暗色/亮色主题切换
|
||||
|
||||
**核心文件**:
|
||||
- `stores/counter.js` — 简单 store 示例
|
||||
- `stores/todo.js` — 完整 CRUD store
|
||||
- `stores/auth.js` — 认证 Token store
|
||||
- `main.js` — `app.use(createPinia())`
|
||||
|
||||
**思考题**:Pinia 和 Vuex 有什么区别?为什么 Vue 3 官方推荐 Pinia?
|
||||
|
||||
---
|
||||
|
||||
## Day 7:Axios 封装与请求拦截
|
||||
|
||||
**知识点**:
|
||||
- `axios.create()`:创建实例,配置 baseURL、timeout
|
||||
- 请求拦截器:自动附加 Token、设置 Content-Type
|
||||
- 响应拦截器:统一处理 401/403/500
|
||||
- `Authorization: Bearer <token>`:JWT 标准格式
|
||||
- Vite proxy 配置:开发环境代理 `/api` 到 Spring Boot
|
||||
- `.env` 环境变量:`VITE_API_BASE_URL`
|
||||
|
||||
**动手 → 理解**:
|
||||
1. 打开 `api/http.js`,对照 Week 5 的 `app.js` 中 `api()` 函数,对比封装方式
|
||||
2. 用 DevTools Network 面板观察请求头中是否有 `Authorization`
|
||||
3. 故意把 token 改错,观察响应拦截器的 401 处理
|
||||
4. 检查 `vite.config.js` 的 proxy 配置,理解开发环境代理原理
|
||||
5. 添加 `VITE_API_BASE_URL` 环境变量,修改 `baseURL` 读取环境变量
|
||||
|
||||
**核心文件**:
|
||||
- `api/http.js` — Axios 实例 + 拦截器
|
||||
- `vite.config.js` → `server.proxy` — 开发代理
|
||||
- `stores/auth.js` — Token 存储与模拟登录
|
||||
|
||||
**思考题**:为什么开发环境需要 Vite proxy,而生产环境用 Nginx 反向代理?
|
||||
|
||||
---
|
||||
|
||||
## Week 6 总结
|
||||
|
||||
| 维度 | 掌握内容 |
|
||||
|------|---------|
|
||||
| 脚手架 | Vite 创建项目、目录结构、HMR 热更新 |
|
||||
| 响应式 | ref / reactive / computed / watch |
|
||||
| 指令 | v-if / v-show / v-for / v-model / v-bind / v-on |
|
||||
| 组件 | SFC、props / emits、slot、scoped style |
|
||||
| 路由 | Vue Router、懒加载、导航守卫 |
|
||||
| 状态 | Pinia Setup Store、storeToRefs |
|
||||
| 网络 | Axios 封装、拦截器、Vite proxy |
|
||||
|
||||
**与 Week 5 的对比**:
|
||||
|
||||
| | Week 5 前端 | Week 6 前端 |
|
||||
|------|-----------|-----------|
|
||||
| 框架 | 原生 HTML + 原生 JS | Vue 3 + Vite |
|
||||
| 状态管理 | localStorage + DOM 操作 | Pinia 响应式 store |
|
||||
| 路由 | 无(单页面切换 display) | Vue Router History 模式 |
|
||||
| HTTP | fetch 手写 | Axios 拦截器封装 |
|
||||
| 组件化 | 无 | SFC + props/emits |
|
||||
| 开发体验 | 手动刷新 | HMR 热更新 |
|
||||
|
||||
**预习 Week 7**:将本项目的 Vue 前端与 Week 5 的 Spring Boot 后端对接,用 Axios 替换模拟登录,实现前后端分离的学生管理系统 v3。
|
||||
Reference in New Issue
Block a user