Skip to content

todoList与在脚手架中使用axios(Lesson8 2/3) ​

本页对应 Lesson08,共 6 段视频,约 1 小时 15 分。

学习目标 ​

  • 能将 TodoList 拆分为 Header、Main、Footer 等组件,并正确迁移公共样式与组件私有样式。
  • 能实现任务添加功能,包括输入校验、重复标题提示、添加成功后清空输入框。
  • 能根据任务 ID 修改选中状态,并实现全选、取消全选、已选数量与全部数量统计。
  • 能根据任务 ID 删除指定任务,并实现鼠标移入显示删除按钮、清除已选任务等底部操作。
  • 能使用 axios 获取任务数据并渲染到页面,同时处理加载中状态与响应体数据提取。
  • 能使用 watch/updated 与 localStorage 实现任务数据持久化,并在页面刷新后恢复数据与选中统计。

知识点 ​

先拆组件:App 只做容器 ​

为什么需要:TodoList 由头部输入、主体列表、底部统计三块组成。如果全写在 App 里,模板和样式会越来越乱;而且头部、主体、底部是兄弟关系,数据不能只在某一个子组件里定义。

怎么想:把 App 当成共同父级,todos 定义在 App 中,三个子组件通过 props 接收数据,通过自定义事件通知 App 修改数据。这样“数据源唯一”,兄弟组件也能共享状态。

怎么做:在 src/components 下建 TodoHeader.vue、TodoMain.vue、TodoFooter.vue,App 中引入并使用:

vue
<template>
  <div id="app">
    <TodoHeader :todos="todos" @add="addTodo" />
    <TodoMain :todos="todos" @update="updateTodo" @del="delTodo" />
    <TodoFooter :todos="todos" @toggle-all="toggleAll" @clear-done="clearDone" />
  </div>
</template>

样式上要注意:scoped 只作用当前组件,子组件内部不一定生效。按钮等公共样式放 src/assets/css/base.css,在 App 或入口中引入:

js
import '@/assets/css/base.css'

易错点:不要把 todos 定义在头部或主体里;不要把子组件样式全留在 App 的 scoped 中;组件名、事件名要统一。

完成添加:判空、判重、清空输入 ​

为什么需要:输入框回车后要把任务加入 todos,但空标题和重复标题不应加入。

怎么想:输入框在 TodoHeader 内,数据在 App 中,所以子组件触发 add,父组件真正修改数组。

怎么做:

vue
<!-- TodoHeader -->
<input v-model="title" @keyup.enter="add" placeholder="请输入任务标题" />
js
methods: {
  add() {
    const text = this.title.trim()
    if (!text) return alert('请输入任务标题')
    this.$emit('add', text)
    this.title = ''
  }
}
js
// App
addTodo(title) {
  const has = this.todos.some(item => item.title === title)
  if (has) return alert('任务标题不允许重复')
  this.todos.push({ id: Date.now(), title, isDone: false })
}

易错点:先 trim() 再判空;判重用 some;添加成功后清空输入框;id 要唯一。

渲染列表与修改选中状态 ​

为什么需要:主体要显示任务,并能勾选完成状态。

怎么想:TodoMain 只负责展示和发事件,不直接改父组件数据。勾选时把 id 传给 App,由 App 更新对应任务。

怎么做:

vue
<!-- TodoMain -->
<li v-for="item in todos" :key="item.id">
  <input type="checkbox" :checked="item.isDone" @change="update(item.id)" />
  <span>{{ item.title }}</span>
</li>
js
// App
updateTodo(id) {
  this.todos = this.todos.map(item =>
    item.id === id ? { ...item, isDone: !item.isDone } : item
  )
}

易错点:不要直接改 props;更新数组时建议返回新数组;调试时可在 App 中打印 todos,确认 isDone 是否变化。

删除任务与移入显示按钮 ​

为什么需要:每行删除按钮默认隐藏,鼠标移入当前行才显示。

怎么想:用一个 activeId 记录当前移入行的 id,初值设为 -1,避免和真实 id 相同。

怎么做:

vue
<li @mouseenter="activeId = item.id" @mouseleave="activeId = -1">
  <button v-show="activeId === item.id" @click="$emit('del', item.id)">删除</button>
</li>
js
delTodo(id) {
  if (!confirm('确定删除该任务?')) return
  this.todos = this.todos.filter(item => item.id !== id)
}

易错点:mouseleave 要放在整行 li 上;activeId 初值不能是已有 id;删除前确认。

底部统计、全选与清除已完成 ​

为什么需要:底部要显示“已选中多少/全部多少”,支持全选和清除已完成。

怎么想:统计值都依赖 todos,可在 TodoFooter 中用 watch 监听 todos,并加 immediate: true 首次计算。

怎么做:

js
data() {
  return { selectedCount: 0, allCount: 0, isAll: false }
},
watch: {
  todos: {
    immediate: true,
    handler() {
      this.selectedCount = 0
      this.allCount = this.todos.length
      this.todos.forEach(item => {
        if (item.isDone) this.selectedCount++
      })
      this.isAll =
        this.allCount > 0 && this.selectedCount === this.allCount
    }
  }
}

全选:

js
toggleAll(checked) {
  this.todos = this.todos.map(item => ({ ...item, isDone: checked }))
}

清除已完成:

js
clearDone() {
  this.todos = this.todos.filter(item => !item.isDone)
}

易错点:watch 中累加前必须清零,否则数量越加越多;空数组时全选应为 false;清除按钮可用 v-show="selectedCount > 0"。

持久化与 axios 获取数据 ​

为什么需要:刷新后数据会丢失,真实项目数据也通常来自接口。

怎么想:本地缓存用 localStorage;初始化 todos 时先读缓存。接口请求用 axios,在 mounted 中获取并渲染。

怎么做:

js
data() {
  return {
    todos: JSON.parse(localStorage.getItem('todos')) || []
  }
},
watch: {
  todos: {
    deep: true,
    handler(val) {
      localStorage.setItem('todos', JSON.stringify(val))
    }
  }
}
js
import axios from 'axios'

mounted() {
  axios.get('接口地址', { params: { q: '参数' } })
    .then(res => {
      this.todos = res.data
    })
    .catch(err => {
      console.error(err)
    })
}

易错点:localStorage 只能存字符串,要 JSON.stringify/parse;接口参数用 params;响应体是 res.data;请求失败要处理。转写中还提到“加载中”,可在请求前设 loading=true,finally 中关闭。

从组件拆分说起:为什么数据要放在父组件 ​

在脚手架里使用 axios 之前,先要理解数据应该放在哪。转写稿里把 App.vue 拆成了三块:头部 TodoHeader、主体 TodoMain、底部 TodoFooter。拆分时把原来 App.vue 里的结构和样式分别剪切到 src/components 下的三个组件中,公共样式抽到 src/assets/css/base.css,再在 App.vue 里通过 import 引入。

为什么要把状态定义在 App.vue?因为头部要添加任务、主体要渲染和修改选中状态、底部要统计和清除,三者是兄弟关系。如果把 todoList 定义在头部组件里,主体和底部就拿不到。所以状态必须提升到三者共同的父组件 App.vue 中,再通过 props 往下传,子组件通过自定义事件把操作“通知”回父组件。

js
// App.vue
data() {
  return {
    todoList: []
  }
}

子组件接收时用 props,转写稿里特别提到:脚手架里 props 可以写成驼峰,不必像早期写法那样强行转成短横线。

js
props: {
  todoList: {
    type: Array,
    required: true
  }
}

安装并引入 axios ​

为什么需要 axios?因为真实项目里数据不可能写死在 App.vue 的数组中,而是来自数据库接口。脚手架中要通过 HTTP 请求拿到数据,axios 就是最常用的请求库。

怎么做?先进入项目目录安装:

bash
cnpm install axios
# 或 npm install axios

然后在需要发请求的组件里引入。转写稿里强调:不引入直接用 axios 会报错。

js
import axios from 'axios'

易错点:安装要在当前项目目录下执行,不是随便一个终端窗口;引入路径写 from 'axios',不要写成相对路径。

在 mounted 中发送 GET 请求 ​

请求一般放在 mounted 里,因为此时组件已经挂载,可以安全地发起网络请求并准备渲染数据。

js
mounted() {
  axios.get('接口地址', {
    params: {
      q: '搜索关键字',
      sort: '排序字段'
    }
  }).then(res => {
    console.log(res.data)
  })
}

转写稿里把查询参数放在第二个参数的 params 对象中,axios 会自动把对象转换成查询字符串拼到地址后面。调试时打开浏览器 Network 面板,能看到请求已经发出,并且有响应体。

获取响应体数据的几种写法 ​

第一种,在 then 回调里直接取 res.data:

js
.then(res => {
  console.log(res.data)
})

第二种,利用解构赋值:

js
.then(({ data }) => {
  console.log(data)
})

第三种,用 async/await:

js
async mounted() {
  const { data } = await axios.get('接口地址', {
    params: { q: '...', sort: '...' }
  })
  console.log(data)
}

转写稿里还提到,如果每次都要写 res.data 觉得麻烦,可以用拦截器统一处理响应。这是后续可以扩展的方向,本节先把请求和取数跑通。

易错点:res 是完整响应对象,里面有 status、headers、data 等,真正业务数据在 res.data 里;不要直接把 res 当成数组去渲染。

调试过程:缓存、监听与计数重置 ​

数据请求回来后要渲染到页面,转写稿里还补了两个实战细节。

一是本地缓存。为了让刷新后数据还在,用 localStorage 存 todoList,初始化时优先读缓存:

js
data() {
  const list = localStorage.getItem('todoList')
  return {
    todoList: list ? JSON.parse(list) : []
  }
}

二是统计“已选中数量”时踩的坑。转写稿里在 watch 或 updated 里遍历 todoList 累加选中项,结果数量越加越大。原因是每次执行都在旧值基础上继续 ++,没有先归零。正确做法是在遍历前先把计数重置为 0,再重新统计。

js
this.selectedCount = 0
this.todoList.forEach(item => {
  if (item.isChecked) this.selectedCount++
})

全选状态也类似:用“选中数量 === 全部数量”判断,但初始时两者都是 0,会误判为全选,需要额外加一个“全部数量大于 0”的条件。

易错点小结 ​

  • props 传数组时类型写 Array,必填写 required: true。
  • 子组件不能直接改父组件数据,要通过 $emit 通知父组件修改。
  • axios.get 的第二个参数是配置对象,查询参数放在 params 里。
  • 响应体是 res.data,不是 res。
  • watch/updated 里做累加统计前,一定先把计数变量重置。
  • localStorage 只能存字符串,存数组前要 JSON.stringify,取出来要 JSON.parse。

为什么要在脚手架里用 axios 获取数据 ​

前面做的待办清单,数据是直接写在 App.vue 的 data 里,增删改查都操作这个本地数组。转写稿里提醒:真正做项目时,数据一般来自数据库或后端接口,不是自己在组件里写死一个数组。所以这一节要练习在脚手架里使用 axios 发请求、拿数据,为后面“获取数据 → 渲染数据 → 加加载中效果”做准备。

这里的“为什么需要”很明确:本地数组只能演示交互,不能代表真实项目的数据流。怎么想?先把 axios 当成一个“发请求的工具”,按“下载依赖 → 引入 → 在组件挂载后发请求 → 从响应里取数据”的顺序做。怎么做?先进入自己的项目目录,再安装 axios。转写稿里特别提到“要进入到你自己的项目当中去下载”,不要在项目外面执行安装命令。

下载并引入 axios ​

笔记里给出的安装命令是:

shell
cnpm install axios

转写稿里说用 npm 也可以,核心是装到当前项目里。装完后,在组件里引入:

js
import axios from "axios";

易错点有两个:一是忘记 import,直接写 axios.get 会报错;二是安装时没进项目目录,依赖装到了别处。转写稿里“贴你入”“A组”这些是口误/同音字,实际就是 import 和 axios。

在 mounted 中发起请求并传参数 ​

为什么放在 mounted?因为组件挂载完成后发请求比较合适,转写稿里“毛蹄子”就是 mounted 的听写误差。怎么想?请求地址是 GitHub 的搜索仓库接口,查询参数有 q 和 sort。不要手写 ?q=r&sort=stars 拼到地址后面,而是把参数放到 axios.get 的第二个参数 params 对象里,axios 会自动拼成查询字符串。转写稿里也解释了:这个对象会被转换成查询字符串,再和地址拼接。

笔记代码如下,保留原样:

vue
<template>
    <div>
        <h3>App</h3>
    </div>
</template>

<script>
import axios from "axios";
export default {
    name: "App",
    async mounted(){
        // 1
        // axios.get("https://api.github.com/search/repositories",{
        //     // ?q=r&sort=stars
        //     params:{
        //         q:"r",
        //         sort:"stars"
        //     }
        // }).then(response=>{
        //     console.log(response.data);
        // })

        // 2
        // axios.get("https://api.github.com/search/repositories",{
        //     // ?q=r&sort=stars
        //     params:{
        //         q:"r",
        //         sort:"stars"
        //     }
        // }).then(({data})=>{
        //     console.log(data);
        // })

        // 3
        const {data} = await  axios.get("https://api.github.com/search/repositories",{
            params: {
                q: "r",
                sort: "stars"
            }
        });
        console.log(data);
    }
}
</script>

<style scoped>

</style>

取响应体的三种写法 ​

转写稿里问“我想得到响应体的内容,你会怎么做”,然后逐步写出三种写法。第一种是 .then(response => console.log(response.data)):response 是整个响应对象,响应体在 response.data 里。第二种是 .then(({data}) => console.log(data)):直接在参数里解构出 data,少写一层。第三种是 async/await 加解构:const {data} = await axios.get(...),然后 console.log(data)。这三种都能拿到响应体,区别只是写法。

易错点:不要把整个 response 当成数据用,真正的数据在 response.data;用 await 时外层函数要加 async,笔记里就是 async mounted();如果只写 axios.get(...) 不 return 也不 await,后面的代码可能先执行。转写稿里还提到可以用拦截器统一处理响应,但本节先把这三种取响应体的方式掌握清楚。

调试过程与小结 ​

调试时可以打开浏览器网络面板,刷新页面,能看到请求已经发出,并且有响应;再在控制台看 data 是否打印出来。转写稿里就是先确认“发送了一个请求”“这边有响应”,再讨论怎么取响应体。实际写的时候,先写 console.log(response) 看结构,再改成 response.data,最后再换成解构或 async/await,这样不容易把响应对象和响应体搞混。

这一节要解决什么问题 ​

前面我们操作的是组件里写死的数组,真实项目里数据通常来自接口。本节的目标是:在 Vue 组件挂载后,用 axios 请求 GitHub 仓库搜索接口,把返回的 data.items 赋给 items,再用 v-for 渲染每个仓库的 full_name。

为什么需要:写死数据只能演示结构,不能练习“请求—取响应体—赋值—渲染”的完整链路。怎么想:先让 items 保持空数组,等组件挂载后再请求,拿到数据再更新视图。怎么做:本节笔记代码先完整保留如下。

html
<template>
    <div>
        <h3>App</h3>
        <div v-for="item in items" :key="item.id">
            <p>{{item.full_name}}</p>
        </div>
    </div>
</template>

<script>
import axios from "axios";
export default {
    name: "App",
    data(){
        return {
            items:[]
        }
    },
    async mounted(){
        const {data} = await  axios.get("https://api.github.com/search/repositories",{
            params: {
                q: "r",
                sort: "stars"
            }
        });
        this.items = data.items;
    }
}
</script>

<style scoped>

</style>

易错点:items 初始值要写成 [],不要写成 null,否则模板遍历容易出问题;接口字段是 full_name,不是驼峰的 fullName。

先安装并引入 axios ​

为什么需要:浏览器里发请求需要请求库,转写稿里明确先下载 axios,再在组件里引入。怎么想:先确认项目目录,再安装;安装完成后在 <script> 顶部 import axios from "axios"。怎么做:在项目目录执行安装命令,例如 npm i axios 或 cnpm i axios,然后在代码里写 import axios from "axios";。易错点:不要在错误目录安装;忘记引入会报 axios is not defined;引入名要和后面使用的 axios.get 一致。

在 mounted 里发请求 ​

为什么需要:组件要先有初始数据,等挂载完成后再发请求,避免模板一开始就依赖不存在的数据。怎么想:转写稿里说“挂载完成以后”使用 mounted;因为要 await,所以写成 async mounted()。怎么做:在 mounted 中调用 await axios.get(...),请求地址是笔记中的 GitHub 搜索地址。易错点:mounted 里用了 await,前面必须加 async;不要写成普通 mounted() 后直接 await,否则语法不通过。调试时转写稿的做法是先看 Network 里请求有没有发出,再看响应体内容。

params 与响应体 data ​

为什么需要:接口需要查询参数 q: "r" 和 sort: "stars"。怎么想:axios 的第二个参数可以放 params 对象,它会帮你拼成查询字符串。怎么做:按笔记写 params: { q: "r", sort: "stars" };请求结果用 const {data} = await axios.get(...) 解构出响应体,再 this.items = data.items。易错点:响应体在 data 里,不是直接 res.items;解构后变量名是 data,赋值时不要写成 this.items = items;q、sort 拼写和大小写要一致。

模板渲染与 key ​

为什么需要:拿到数组后要逐项显示仓库名。怎么想:v-for="item in items" 遍历,每一项用 item.full_name 输出;key 用接口提供的唯一 item.id。怎么做:模板按笔记写 :key="item.id" 和 {{item.full_name}}。易错点:key 要唯一,这里用 item.id 是合适的;字段名不要写错;如果页面空白,先确认 items.length 是否大于 0,再确认 full_name 是否存在。

调试顺序小结 ​

转写稿的调试思路可以归纳为:先安装并引入 axios,再在 mounted 中发请求;打开网络面板确认请求和参数;查看响应体,用解构拿到 data;最后把 data.items 赋给组件状态,让模板重新渲染。遇到不显示时,按“请求是否发出 → 响应体是否有数据 → items 是否赋值 → 模板字段是否写对”的顺序排查。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

1. App.vue —— 通过 axios 获取数据(脚手架中) ​

vue
<template>
    <div>
        <h3>App</h3>
    </div>
</template>

<script>
import axios from "axios";
export default {
    name: "App",
    async mounted(){
        // 1
        // axios.get("https://api.github.com/search/repositories",{
        //     // ?q=r&sort=stars
        //     params:{
        //         q:"r",
        //         sort:"stars"
        //     }
        // }).then(response=>{
        //     console.log(response.data);
        // })

        // 2
        // axios.get("https://api.github.com/search/repositories",{
        //     // ?q=r&sort=stars
        //     params:{
        //         q:"r",
        //         sort:"stars"
        //     }
        // }).then(({data})=>{
        //     console.log(data);
        // })

        // 3
        const {data} = await  axios.get("https://api.github.com/search/repositories",{
            params: {
                q: "r",
                sort: "stars"
            }
        });
        console.log(data);
    }
}
</script>

<style scoped>

</style>

关键行与易错点

  • 先在项目根目录安装依赖:cnpm install axios(npm install axios 亦可),否则 import axios from "axios" 会报模块找不到。
  • params 对象会被 axios 自动拼成查询字符串 ?q=r&sort=stars,不需要自己手动拼 URL。
  • 写法 1 的 response.data 与写法 2 的解构 ({data}) 是同一个东西:data 才是响应体,response 是整个响应对象。
  • 写法 3 用了 await,所以 mounted 前面必须加 async,漏写 async 会直接语法报错。
  • 本节是“脚手架中”的用法,组件必须放在 Vue CLI 工程里运行,不能直接当普通 HTML 打开。

2. App.vue —— 渲染 axios 获取到的数据 ​

vue
<template>
    <div>
        <h3>App</h3>
        <div v-for="item in items" :key="item.id">
            <p>{{item.full_name}}</p>
        </div>
    </div>
</template>

<script>
import axios from "axios";
export default {
    name: "App",
    data(){
        return {
            items:[]
        }
    },
    async mounted(){
        const {data} = await  axios.get("https://api.github.com/search/repositories",{
            params: {
                q: "r",
                sort: "stars"
            }
        });
        this.items = data.items;
    }
}
</script>

<style scoped>

</style>

关键行与易错点

  • data() 必须 return 一个对象,items 初始值给空数组,模板里 v-for 才不会在首次渲染时报错。
  • 接口返回的列表在 data.items 里,所以赋值是 this.items = data.items,写成 this.items = data 会把整个响应体(含 total_count 等)当成数组用,渲染就出错。
  • v-for 必须写 :key,这里用 item.id;用索引当 key 在数据增删时容易出问题。
  • 模板里取的是 item.full_name(GitHub 接口字段),字段名写错不会报错,只会渲染空白。
  • async mounted 中赋值用 this.items,不要写成 items = ...,否则不会触发响应式更新。

常见错误 ​

  1. 拆分组件后子组件样式全部失效

    • 现象:把头部/主体/底部的 DOM 拆出去后,页面变成无样式的裸结构,控制台无报错。
    • 原因:样式原封不动留在 App 的 <style scoped> 里,scoped 只作用于当前组件模板内的元素,不会穿透到子组件内部。
    • 修法:按“谁的 DOM 谁的样式”把样式搬进对应组件;多个组件共用的按钮、li 等抽到 src/assets/base.css,在 main.js 或 App 中全局引入。
  2. 父组件传值给子组件,子组件拿到 undefined

    • 现象:props 里声明了 todoList,子组件遍历时报 Cannot read property 'length' of undefined。
    • 原因:模板中属性名大小写被 HTML 规则吞掉(:todoList 与 :todo-list 不一致),或父组件根本没写绑定,只写了静态属性。
    • 修法:props 声明与模板传值保持同一套命名(推荐 props 驼峰、模板短横线);给 props 写 type 和 default: () => [],让错误在开发阶段就暴露。
  3. 勾选后视图不刷新,数据里其实已经改了

    • 现象:点 checkbox,console.log 里 isTest 变了,但页面上的勾选状态和统计数字不动。
    • 原因:直接修改了 props 数组里的对象属性(item.isTest = !item.isTest),既违反单向数据流,又踩到 Vue2 对数组索引/对象属性的响应式盲区。
    • 修法:map 出一份新数组,改完通过 $emit('update:todoList', newList) 交给父级替换;父级用新引用赋值,视图才会更新。
  4. “已选中数量”越点越大

    • 现象:第一次勾选显示 1,再勾一个变 3,再勾变 6。
    • 原因:watch 回调里用 this.selectedName++ 累加,但每次回调前没有把计数器归零,上一轮的结果被带进了下一轮。
    • 修法:回调第一行先 this.selectedName = 0 再遍历;更稳妥的是改用 computed,每次从 todoList 重新 filter().length,天然没有残留。
  5. 空列表时“全选”被自动勾上

    • 现象:刚进页面一条任务都没有,全选框却是选中态。
    • 原因:判断条件只写了 selectedName === allName,0 === 0 成立。
    • 修法:补上数量大于 0 的前提,即“有任务且已选数等于总数”才算全选;isAll 用 computed 表达,不要在多处手动赋值。
  6. 加了 localStorage 后刷新页面数据回来了,统计数字却是 0

    • 现象:列表能恢复,但“已选中/全部”显示 0,全选框状态也不对。
    • 原因:只做了“写缓存”,data 初始值仍是空数组;即使读了缓存,watch 也是数据变化后才执行,首次进入不会触发统计。
    • 修法:data 初始化时从 localStorage 读取并 JSON.parse,读不到再给空数组;统计逻辑加 immediate: true,保证首屏算一次。
  7. mounted 里用 await 却拿不到数据

    • 现象:const { data } = await axios.get(...) 报错,或 data 是 undefined。
    • 原因:mounted 前漏写 async;或者写成了 axios.get(...).then() 又在外面 await,两种风格混用。
    • 修法:async mounted() 与 await 配套使用;要取响应体就解构 { data },不要直接打印整个 response。
  8. 全选/反选点了没反应

    • 现象:点全选框,map 里也执行了,但列表勾选状态不变。
    • 原因:map 之后忘了 $emit 把新数组交回父级,或事件名大小写与父组件监听的不一致(如 updata / update、T 写成大写)。
    • 修法:map 完必须 $emit;事件名统一小写短横线并在父子两端对照检查,命名错误是这类“逻辑没错但不生效”的高频原因。

练习 ​

  1. 组件拆分与样式归属:把现有 todoList 拆成 TodoHeader、TodoMain、TodoFooter 三个组件,同时把样式按归属迁移,抽出一个 base.css 放公共按钮样式。完成后在笔记里列出“每条样式为什么放在这个文件”。

  2. 添加任务的完整链路:在 TodoHeader 中实现回车添加,要求覆盖空值提示、重复标题提示、添加成功后清空输入框三种情况。状态定义在 App,子组件只负责收集输入并 $emit,说明为什么状态不能放在 Header 里。

  3. 把 watch 版统计改写成 computed 版:分别实现“已选中数量、任务总数、是否全选”三个派生值,先用 watch 写一版,再用 computed 写一版,对比两种写法在“首次进入”和“删除一条已选任务”两个场景下的表现差异。

  4. 本地持久化:为 todoList 增加 localStorage 缓存,要求刷新后列表、已选数量、全选状态三者一致;再手动清空缓存验证首次进入不会报错。

  5. axios 数据渲染 + 加载态:用 async/await 请求 GitHub 仓库搜索接口,把结果渲染成列表,同时实现 loading 与请求失败提示。要求请求参数通过 params 对象传递,不要手拼查询字符串。

原视频对照 ​

#视频时长
1对组件进行拆分07:24
2完成添加11:39
3完成修改选中的状态09:16
4根据ID删除任务07:35
5完成底部操作mp429:44
6通过axios获取数据09:37

小结 ​

  1. 组件拆分的核心不是拆文件,而是先确定“状态放在共同父级、子组件只负责展示和发事件”这条边界,边界定错后面全是补丁。
  2. 样式要跟着 DOM 走,公共样式全局化;scoped 不是万能胶,跨组件复用样式必须显式提升到全局。
  3. 凡是操作数组,一律“复制—修改—回传—替换引用”,不要在原数组和 props 上就地改,这是响应式失效和单向数据流告警的共同根源。
  4. 派生数据(数量、全选、是否为空)优先用 computed 表达,watch 只留给副作用(写缓存、发请求),否则很容易写出“忘记归零”的累加 bug。
  5. 持久化是“读 + 写”两件事,只做一半就会出现“数据回来了、界面没回来”的假成功;初始化时机和 immediate 要一起考虑。
  6. 异步请求要形成固定套路:async + await + try/catch/finally,loading 和错误提示是标配而不是可选项。
  7. 这一节出现的多数“功能不生效”,根因是命名不一致(props、事件名、大小写)和漏写 $emit,排查时应先看父子两端的契约,再怀疑逻辑。
  8. 从本地数组到接口数据,变化的只是数据来源,组件结构、状态归属和更新方式应当保持不变——结构稳了,换数据源才只是换一个函数的事。