使用Vue开发ToDoList官网

420 阅读17分钟

ToDoList练习讲解

此文章仅用于对本人爱子的学习指导,如有实现此功能更加好的实现方式欢迎通过 issue 指正。

如果您也和我的爱子一样正在学习 vue 基础,欢迎点击这里前往获取此模版参与到我们的学习当中。

又或者您对此文章的实现有自己独立的见解也欢迎前往这里发表您宝贵的意见。

第一章:知识点讲解

在讲解如何实现 ToDoList 的功能之前,我们不妨先了解一下实现它需要具备哪些知识点,并且我们可以在这里快速的去了解并且掌握它。

如果您对以下所述的所有知识点都已经掌握,那么本人建议您可以直接前往第二章节查阅:

一、数组方法

1、filter

2、reduce

3、at

4、map

5、unshift

6、splice

二、Vue中的选项Api

1、el

2、data

3、methods

4、computed

5、components

1.1 数组的常用高阶方法

1.1.1 unshift

unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度(该方法修改原有数组)。

语法规则:

array.unshift(value1[, value2, ..., valueN])

参数:

valueN

要添加到数组开头的元素

返回值:

当一个对象调用该方法时,返回其 length 属性值。

代码实例:

const arr = [1, 2, 3, 4, 5];
​
console.log(arr.unshift(-1, 0)); // 返回值为数组最新的长度 7
​
console.log(arr); // [-1, 0, 1, 2, 3, 4, 5]

1.1.2 splice

splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。

语法规则:

array.splice(start[, deleteCount[, item1, ..., itemN])

参数:

start

指定修改的开始位置(从 0 计数)。如果超出了数组的长度,则从数组末尾开始添加内容;如果是负值,则表示从数组末位开始的第几位(从 -1 计数,这意味着 -n 是倒数第 n 个元素并且等价于 array.length-n);如果负数的绝对值大于数组的长度,则表示开始位置为第 0 位。

deleteCount

整数,表示要移除的数组元素的个数。如果 deleteCount 大于 start 之后的元素的总数,则从 start 后面的元素都将被删除(含第 start 位)。如果 deleteCount 被省略了,或者它的值大于等于array.length - start(也就是说,如果它大于或者等于start之后的所有元素的数量),那么start之后数组的所有元素都会被删除。如果 deleteCount 是 0 或者负数,则不移除元素。这种情况下,至少应添加一个新元素。

item1, item2, ...

要添加进数组的元素,从start 位置开始。如果不指定,则 splice() 将只删除数组元素。

返回值:

由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。

代码实例:

const months = ['Jan', 'March', 'April', 'June'];
​
console.log(months.splice(1, 0, 'Feb')); // 返回值为由被删除元素所形成的新数组 []
​
console.log(months); // ['Jan', 'Feb', 'March', 'April', 'June']

1.1.3 at

at() 方法接收一个整数值并返回该索引的项目,允许正数和负数。负整数从数组中的最后一个项目开始倒数。

方括号符号没有问题。例如,array[0]将返回第一个项目。然而,对于后面的项目,不要使用array.length,例如,对于最后一个项目,可以调用array.at(-1)

语法规则:

array.at(index)

参数:

index

要返回的数组元素的索引(位置)。当传递负数时,支持从数组末端开始的相对索引;也就是说,如果使用负数,返回的元素将从数组的末端开始倒数。

返回值:

匹配给定索引的数组中的元素。如果找不到指定的索引,则返回undefined

代码实例:

const arr = ['apple', 'banana', 'pear'];
​
// 传统方式获取数组的最后一个元素
let lastV = arr[arr.length - 1]; // 'pear'
​
// 通过 at 获取数组的最后一个元素
lastV = arr.at(-1); // 'pear'

1.1.4 map

map() 方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。

语法规则:

const newArr = arr.map(function callback(value, index, array) {}[, thisArg])

参数:

callback

生成新数组元素的函数,使用三个参数:

value

数组中正在处理的当前元素。

index

数组中正在处理的当前元素的索引。

array

map方法调用的数组。

thisArg

执行 callback 函数时值被用作this

返回值:

一个由原数组每个元素执行回调函数的结果组成的新数组。

方法描述:

map 方法会给原数组中的每个元素都按顺序调用一次 callback 函数。callback 每次执行后的返回值(包括 undefined)组合起来形成一个新数组。 callback 函数只会在有值的索引上被调用;那些从来没被赋过值或者使用 delete 删除的索引则不会被调用。

因为map生成一个新数组,当你不打算使用返回的新数组却使用map是违背设计初衷的,请用forEach或者for-of替代。你不该使用map: A) 你不打算使用返回的新数组,或/且 B) 你没有从回调函数中返回值。

callback 函数会被自动传入三个参数:数组元素,元素索引,原数组本身。

如果 thisArg 参数提供给map,则会被用作回调函数的this值。否则 undefined 会被用作回调函数的this值。this的值最终相对于callback函数的可观察性是依据the usual rules for determining the this seen by a function决定的

map 不修改调用它的原数组本身(当然可以在 callback 执行时改变原数组)

map 方法处理数组元素的范围是在 callback 方法第一次调用之前就已经确定了。调用map方法之后追加的数组元素不会被callback访问。如果存在的数组元素改变了,那么传给callback的值是map访问该元素时的值。在map函数调用后但在访问该元素前,该元素被删除的话,则无法被访问到。

代码示例:

求每个元素的平方根

const arr = [1, 2, 3];
​
arr.map(function (v) {
  return v ** 2
});
​
// or
​
arr.map(Math.sqrt);

1.1.5 filter

filter() 方法创建一个新数组,其包含通过所提供函数实现的测试的所有元素。

语法规则:

const newArray = arr.filter(callback(element[, index, array])[, thisArg])

参数:

callback

用来测试数组的每个元素的函数。返回 true 表示该元素通过测试,保留该元素,false 则不保留。它接受以下三个参数: element 数组中当前正在处理的元素。 index可选 正在处理的元素在数组中的索引。

array

  调用了 `filter` 的数组本身。   

thisArg

执行 callback 时,用于 this 的值。

返回值

一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。

描述:

filter 为数组中的每个元素调用一次 callback 函数,并利用所有使得 callback 返回 true 或等价于 true 的值的元素创建一个新数组。callback 只会在已经赋值的索引上被调用,对于那些已经被删除或者从未被赋值的索引不会被调用。那些没有通过 callback 测试的元素会被跳过,不会被包含在新数组中。

callback 被调用时传入三个参数:

  1. 元素的值
  2. 元素的索引
  3. 被遍历的数组本身

如果为 filter 提供一个 thisArg 参数,则它会被作为 callback 被调用时的 this 值。否则,callbackthis 值在非严格模式下将是全局对象,严格模式下为 undefinedcallback 函数最终观察到的 this 值是根据通常函数所看到的 "this"的规则确定的。

filter 不会改变原数组,它返回过滤后的新数组。

filter 遍历的元素范围在第一次调用 callback 之前就已经确定了。在调用 filter 之后被添加到数组中的元素不会被 filter 遍历到。如果已经存在的元素被改变了,则他们传入 callback 的值是 filter 遍历到它们那一刻的值。被删除或从来未被赋值的元素不会被遍历到。

代码示例:

获取考试成绩在60分以上的全部记录

const score = [0, 100, 70, 50, 10];
​
const newScore = score.filter(function (s) {
  return s > 60;
});

1.1.6 reduce

reduce()方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。

语法规则:

const result = array.reduce(function reducer([prev, next, index, array]) {}[, initPrev])

参数:

prev

上一次调用 reducer 时的返回值。在第一次调用时,若指定了初始值 initPrev,其值则为 initPrev,否则为数组索引为 0 的元素 array[0]

next

数组中正在处理的元素。在第一次调用时,若指定了初始值 initPrev,其值则为数组索引为 0 的元素 array[0],否则为 array[1]

index

数组中正在处理的元素的索引。若指定了初始值 initPrev,则起始索引号为 0,否则从索引 1 起始。

initPrev

作为第一次调用 reducer 函数时参数 prev 的值。若指定了初始值 initPrev,则 next 将使用数组第一个元素;否则 prev 将使用数组第一个元素,而 next 将使用数组第二个元素。

返回值:

使用 reducer 回调函数遍历整个数组后的结果。

注意:

数组为空且初始值 initPrev 未提供时会产生异常。

代码示例:

1、实现数组元素累加求和

const arr = [1, 2, 3, 4, 5];
​
const sum = arr.reduce(function (total, value) {
  return total += value;
});

2、实现深层次累加求和

const arr = [
  { id: 1, num: 10 },
  { id: 2, num: 11 },
  { id: 3, num: 12 }
];

arr.reduce(function (total, value){
  return total += value.num;
}, 0);

1.2 Vue2中的常用选项Api

1.2.1 el

提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标。可以是 CSS 选择器,也可以是一个 HTMLElement 实例。

const app = new Vue({
  el: '#app'
})

如果在实例化时存在这个选项,实例将立即进入编译过程,否则,需要显式调用 app.$mount() 手动开启编译,我们在实际开发当中更多是手动去开启模版编译的,所以我们可以将上述代码修改为如下:

const app = new Vue({})
​
app.$mount('#app')

1.2.2 data

Vue 实例的数据对象。Vue 会递归地把 data 的 property 转换为 getter/setter,从而让 data 的 property 能够响应数据变化。正常来说,data 应该只能是数据 - 不推荐观察拥有状态行为的对象。

const app = new Vue({
  data: {
    message: '你好呀!!!' 
  }
})
​
app.$mount('#app')

_$ 开头的 property 不会被 Vue 实例代理,因为它们可能和 Vue 内置的 property、API 方法冲突。你可以使用例如 app.$data._property 的方式访问这些 property。

const app = new Vue({
  data: {
    _message: '你好呀!!!' 
  }
})
​
app.$mount('#app')
​
console.log(app.$data._message) // '你好呀!!!'

1.2.3 methods

methods 将被混入到 Vue 实例中。可以直接通过 VM 实例访问这些方法,或者在指令表达式中使用。方法中的 this 自动绑定为 Vue 实例。

const app = new Vue({
  data: {
    message: '你好呀!!!'
  },
  methods: {
    say () {
      console.log(this.message) // '你好呀!!!'
    }
  }
})
​
app.$mount('#app')

1.2.4 computed

计算属性将被混入到 Vue 实例中。所有 getter 和 setter 的 this 上下文自动地绑定为 Vue 实例。

const app = new Vue({
  data: {
    nums: 0
  },
  computed: {
    // 仅读取
    recordNumber () {
      return this.a + 1
    },
    // 读取和设置
    recordNumberPlus: {
      get () {
        return this.a + 1
      },
      set (v) {
        this.a = v + 1
      }
    }
  }
})
​
app.$mount('#app')
​
app.recordNumber // 2
​
app.recordNumberPlus // 2
​
app.recordNumberPlus = 3
​
app.recordNumberPlus // 5

注意如果你为一个计算属性使用了箭头函数,则 this 不会指向这个组件的实例,不过你仍然可以将其实例作为函数的第一个参数来访问。

const app = new Vue({
  data: {
    nums: 0
  },
  computed: {
    recordNumber: app => app.a + 1
  }
})
​
app.$mount('#app')

计算属性的结果会被缓存,除非依赖的响应式 property 变化才会重新计算。注意,如果某个依赖 (比如非响应式 property) 在该实例范畴之外,则计算属性是不会被更新的。

1.2.5 components

包含 Vue 实例可用组件的哈希表。用于注册局部组件。

<!-- 定义组件的模板 -->
<template id="cpn">
  <div>我是一个组件</div>
</template><div id="app">
  <!-- cpn标签最终会被渲染为对应模板中的所有元素 -->
  <cpn></cpn>
</div><script>
  new Vue({
    components: {
      cpn: {
        template: '#cpn' // 模板的类名
      }
    }
  }).$mount('#app')
</script>

组件和vue实例一样,拥有着很多相同的选项Api,值得注意的是,组件中的data选项只能在该组件内部被访问到,且组件中的data选项应该是一个函数,并且这个data函数应该返回一个新的对象。

<!-- 定义组件的模板 -->
<template id="cpn">
  <!-- 成功 -->
  <div>{{ message }}</div>
</template><div id="app">
  <!-- 失败,会报错! -->
  {{ message }}
  <!-- cpn标签最终会被渲染为对应模板中的所有元素 -->
  <cpn></cpn>
</div><script>
  new Vue({
    components: {
      cpn: {
        template: '#cpn', // 模板的类名
        data: () => ({
          message: '组件中的message'
        })
      }
    }
  }).$mount('#app')
</script>

第二章:实现一个简单的 ToDoList

在通过下载此项目模板后,我们通过运行这个项目可以看到如下效果。

如果出现无法运行此项目的情况,可以尝试如下方法:

本人是通过vscode进行运行的项目,并且通过使用vscode中的open with live server插件开启本地服务运行的此项目。windows中可以在选中index.html文件后通过快捷键alt + l + o运行,MAC用户可以在选中index.html文件后通过快捷键command + l + o运行。

图片.png

2.1 实现添加代办事项

功能需求: 当用户在输入框中输入内容并按下回车键时,将用户输入的内容添加至正在进行区域内。

通过上述需求,我们可以先行给id为title的输入框添加一个双向数据绑定:

<!-- ToDoList-template/index.html -->
<header>
  <section>
    <label for="title">{{ title }}</label>
    <input
      type="text"
      id="title"
      name="title"
      placeholder="添加ToDo"
      required="required"
      autocomplete="off"
      v-model="content"
    />
  </section>
</header>

并将content变量设置到data选项上:

// ToDoList-template/js/main.js
window.onload = () => {
  const app = new Vue({
    data: {
      // 网页标题
      title: 'ToDoList',
      // 代办事项内容
      content: ''
    }
  }).$mount('#app')
  // 不清楚 v-model 的可以通过给content赋值查看效果
  app.content = 'Hello'
}

这个时候我们可以通过打开浏览器查看相应的网页效果:

图片.png

我们可以清楚看到上面的搜索框中已经获取到了content中的值,而且当我们手动去修改输入框的内容时,content变量也会接收到输入框中更新后的结果,这里我就不带着你们演示了。

接下来,我们实现监听用户在按下回车键之后获取输入框的结果,键盘按下事件我们可以通过v-on:keyup进行监听,同时也可以通过简化后的写法@keyup进行监听,监听用户按下回车事件可以通过事件修饰符enter实现,代码实现如下所示:

<!-- ToDoList-template/index.html -->
<header>
  <section>
    <label for="title">{{ title }}</label>
    <input
      type="text"
      id="title"
      name="title"
      placeholder="添加ToDo"
      required="required"
      autocomplete="off"
      v-model="content"
      @keyup.enter="addToDo"
    />
  </section>
</header>

如上述代码所示,我们给输入框绑定了一个键盘按下事件,并在用户按下回车键之后响应事件,也就是用户按下回车键后会自动调用addToDo函数,当然这个函数我们还没有声明,接下来我们继续完善代码:

// ToDoList-template/js/main.js
window.onload = () => {
  const app = new Vue({
    data: {
      title: 'ToDoList',
      content: ''
    },
    methods: {
      // 用户按下回车键之后需要响应的事件处理函数
      addToDo () {
        console.log(this.content)
      }
    }
  }).$mount('#app')
}

在这里我们并没有做过多的逻辑处理,而是尝试打印输入框中的内容,我们打开浏览器查看一下是否实现:

屏幕录制2022-06-18 00.07.24.gif

此时,我们发现我们已经拿到了我们想要的数据了,接下来我们将拿到的数据渲染到正在进行栏目中:

// ToDoList-template/js/main.js
window.onload = () => {
  const app = new Vue({
    data: {
      title: 'ToDoList',
      content: '',
      // 代办事项列表,记录所有的代办事项
      todos: []
    },
    methods: {
      addToDo () {
        // 输入框中存在内容时才能进行添加操作
        if (this.content) {
          // 往代办事项列表中添加一条记录
          this.todos.unshift({ id: this.todos.length, content: this.content, done: false })
          // 重置输入框中的内容
          this.content = ''
          // 让输入框失去光标
          this.$refs.input.blur()
        }
      }
    }
  }).$mount('#app')
}

通过阅读addToDo事件处理函数中的代码逻辑,我们可以清楚的知道在用户按下回车键之后程序将会做些什么。可能没有接触过组件这个概念的同学在看完第一章节的内容时并不能很好的去理解this.$refs是什么,不理解的同学可以通过前往vue官网进行学习。官方给出了一个解释:一个对象,持有被注册过ref属性的所有DOM元素和组件实例,大致可以认为是当通过给html元素或者vue组件定义一个ref属性之后,我们可以通过根据这个属性去获取到这个html元素或者vue组件实例。当然,我们还没有去定义这个ref属性,现在我们前往index.html文件去操作:

<!-- ToDoList-template/index.html -->
<header>
  <section>
    <label for="title">{{ title }}</label>
    <input
      type="text"
      id="title"
      name="title"
      ref="input"
      placeholder="添加ToDo"
      required="required"
      autocomplete="off"
      v-model="content"
      @keyup.enter="addToDo"
    />
  </section>
</header>

注意:定义的ref属性值将会作为app$refs对象上的其中一个属性,然后我们就可以通过对象[属性]的方式获取到这个html元素或者组件实例,如前面所示的this.$refs.input,在这里因为inputhtml元素上的ref属性值,所以这里拿到的是一个元素,然后通过这个元素调用blur方法实现让元素本身失去光标。

为了让页面上产生效果,我们再对正在进行事项下的的固定代码进行修改:

<h2>
  正在进行
  <span id="todocount">1</span>
</h2>
<ol id="todolist" class="demo-box">
  <li
    v-for="todo in todos"
    :key="todo.id"
  >
    <input type="checkbox" />
    <p>{{ todo.content }}</p>
    <a href="javascript:;"></a>
  </li>
</ol>

此时,我们研究一下上述代码更新的地方,不难发现我们将代办事项列表中的所有数据通过vue的v-for指令循环渲染成为了一个一个的li,并且将里面的数据渲染到p元素上。此时,我们可以打开浏览器调试并查看效果:

屏幕录制2022-06-18 01.00.43.gif

好的,现在我们已经实现了添加代办事项的功能!!!

2.2 实现修改代办事项的状态

功能需求: 当用户勾选上正在进行中的代办事项时,被选中的代办事项将会移至已经完成分组中。反之,如果已经完成中的代办事项被取消选中时,将会被移至正在进行的分组中。且在事项发生移动时,均会自动移至该分组的最前面。

此处,根据需求我们需要对代办事项进行分组处理,在前面我们新增todos的数据时,我们会自动传入一个属性done并赋予其值为false,代表了这条数据应该被加入到正在进行分组中。这就意味着当我们对数据中的done属性进行修改时,则相当于修改了其代办事项的状态。

根据上述想法,我们单独定义两个计算属性用来分别获得正在进行已经完成分组的数据。

window.onload = () => {
  const app = new Vue({
    data: {
      title: 'ToDoList',
      content: '',
      todos: []
    },
    methods: {
      addToDo () {
        if (this.content) {
          this.todos.unshift({ id: this.todos.length, content: this.content, done: false })
          this.content = ''
          this.$refs.input.blur()
        }
      }
    },
    computed: {
      // 收集已经完成的代办事项
      dones: app => app.todos.filter(todo => todo.done),
      // 收集正在进行的代办事项
      doings: app => app.todos.filter(todo => !todo.done)
    }
  }).$mount('#app')
}

在收集完对应数据后,我们将数据渲染到对应分组上:

<h2>
  正在进行
  <span id="todocount">{{ doings.length }}</span>
</h2>
<ol id="todolist" class="demo-box">
  <li
    v-for="todo in doings"
    :key="todo.id"
  >
    <input type="checkbox" />
    <p>{{ todo.content }}</p>
    <a href="javascript:;"></a>
  </li>
</ol><h2>
  已经完成
  <span id="donecount">{{ dones.length }}</span>
</h2>
<ul id="donelist">
  <li
    v-for="todo in dones"
    :key="todo.id"
  >
    <input type="checkbox" checked />
    <p>{{ todo.content }}</p>
    <a href="javascript:;"></a>
  </li>
</ul>

从上述代码中,我们通过doings.lengthdones.length分别获取正在进行已经完成分组中的数据条数,并渲染在页面上。

此刻,我们只需通过给所有的input复选框绑定一个change事件监听状态是否发生改变,然后进行对应的逻辑处理即可完成更新代办事项的状态:

<h2>
  正在进行
  <span id="todocount">{{ doings.length }}</span>
</h2>
<ol id="todolist" class="demo-box">
  <li
    v-for="todo in doings"
    :key="todo.id"
  >
    <input type="checkbox" @change="updateState(todo.id, $event)" />
    <p>{{ todo.content }}</p>
    <a href="javascript:;"></a>
  </li>
</ol><h2>
  已经完成
  <span id="donecount">{{ dones.length }}</span>
</h2>
<ul id="donelist">
  <li
    v-for="todo in dones"
    :key="todo.id"
  >
    <input type="checkbox" checked @change="updateState(todo.id, $event)" />
    <p>{{ todo.content }}</p>
    <a href="javascript:;"></a>
  </li>
</ul>

根据上述代码,我们将change事件绑定到了所有的复选框当中,并在触发状态改变时调用updateState事件处理函数,在updateState中传递了两个参数,分别为todo.id$event,前者为当前代办事项的唯一标识,后者为该事件被触发时由该事件自主传递的事件对象。原本,事件对象event是会自动传入的,但是,由于我们在调用此事件处理函数时传入了一个我们的参数,这个参数会覆盖默认给我们传递的事件对象event,如果在后续我们还需要在事件处理函数中获得并使用它,我们需要手动传入一个$event参数。现在,我们先去声明updateState事件处理函数:

​
window.onload = () => {
  const app = new Vue({
    data: {
      title: 'ToDoList',
      content: '',
      todos: []
    },
    methods: {
      addToDo () {
        if (this.content) {
          this.todos.unshift({ id: this.todos.length, content: this.content, done: false })
          this.content = ''
          this.$refs.input.blur()
        }
      },
      // 更新代办事项状态。id 为当前修改状态的代办事项的唯一标识,e 为事件对象
      updateState (id, e) {
        // 记录需要修改状态的代办事项在代办事项列表中的位置
        let index
        // 更新代办事项列表中的数据
        this.todos = this.todos.map((todo, i) => {
          if (todo.id === id) {
            // 保存被修改状态的代办事项最新的状态
            todo.done = e.target.checked
            // 获得被修改状态的代办事项在代办事项列表中的位置
            index = i
          }
          return todo
        })
​
        // 移除被修改状态的代办事项并获得它
        const target = this.todos.splice(index, 1).at(0)
        // 将移除的代办事项插入到代办事项列表的最上方
        this.todos.unshift(target)
      }
    },
    computed: {
      dones: app => app.todos.filter(todo => todo.done),
      doings: app => app.todos.filter(todo => !todo.done)
    }
  }).$mount('#app')
}

在上述代码中,我们可以通过事件对象提供给我们的target属性获取到修改了状态的复选框,然后通过这个调用元素此身上的checked属性获取到最新的选中状态并更新到代办事项列表todos中。接下来,我们可以查看一下效果:

屏幕录制2022-06-18 01.53.41.gif

恭喜恭喜!现在我们已经完成了代办事项的更新操作。

2.3 实现删除和清空代办事项

功能需求: 用户在点击对应代办事项的删除按钮时会删除掉对应代办事项的卡片,在点击clear按钮时应当能够清空所有的代办事项。

根据上述需求,我们首先给删除按钮绑定对应的点击事件并传递其唯一标识实现精确删除:

<h2>
  正在进行
  <span id="todocount">{{ doings.length }}</span>
</h2>
<ol id="todolist" class="demo-box">
  <li
    v-for="todo in doings"
    :key="todo.id"
  >
    <input type="checkbox" @change="updateState(todo.id, $event)" />
    <p>{{ todo.content }}</p>
    <!-- 添加点击事件 -->
    <a href="javascript:;" @click="dropToDo(todo.id)"></a>
  </li>
</ol><h2>
  已经完成
  <span id="donecount">{{ dones.length }}</span>
</h2>
<ul id="donelist">
  <li
    v-for="todo in dones"
    :key="todo.id"
  >
    <input type="checkbox" checked @change="updateState(todo.id, $event)" />
    <p>{{ todo.content }}</p>
    <!-- 添加点击事件 -->
    <a href="javascript:;" @click="dropToDo(todo.id)"></a>
  </li>
</ul>

如上代码所述,在用户点击对应的删除按钮时会触发dropToDo事件处理函数,接下来我们先定义这个函数:

​
window.onload = () => {
  const app = new Vue({
    data: {
      title: 'ToDoList',
      content: '',
      todos: []
    },
    methods: {
      addToDo () {
        if (this.content) {
          this.todos.unshift({ id: this.todos.length, content: this.content, done: false })
          this.content = ''
          this.$refs.input.blur()
        }
      },
      updateState (id, e) {
        let index
​
        this.todos = this.todos.map((todo, i) => {
          if (todo.id === id) {
            todo.done = e.target.checked
            index = i
          }
          return todo
        })
​
        const target = this.todos.splice(index, 1).at(0)
​
        this.todos.unshift(target)
      },
      // 删除代办事项
      dropToDo (id) {
        this.todos = this.todos.filter(todo => todo.id !== id)
      }
    },
    computed: {
      dones: app => app.todos.filter(todo => todo.done),
      doings: app => app.todos.filter(todo => !todo.done)
    }
  }).$mount('#app')
}

实现移除数组元素的方法有很多种,这里采用filter筛选掉不符合条件的元素从而实现删除的操作。

清空所有的代办事项:

<footer>
  Copyright &copy; 2014 todolist.cn
  <a href="javascript:;" @click="clear">clear</a>
</footer>

如上代码所述,在用户点击清空按钮时会触发clear事件处理函数,接下来我们先定义这个函数:

​
window.onload = () => {
  const app = new Vue({
    data: {
      title: 'ToDoList',
      content: '',
      todos: []
    },
    methods: {
      addToDo () {
        if (this.content) {
          this.todos.unshift({ id: this.todos.length, content: this.content, done: false })
          this.content = ''
          this.$refs.input.blur()
        }
      },
      updateState (id, e) {
        let index
​
        this.todos = this.todos.map((todo, i) => {
          if (todo.id === id) {
            todo.done = e.target.checked
            index = i
          }
          return todo
        })
​
        const target = this.todos.splice(index, 1).at(0)
​
        this.todos.unshift(target)
      },
      dropToDo (id) {
        this.todos = this.todos.filter(todo => todo.id !== id)
      },
      // 清空代办事项
      clear () {
        this.todos = []
      }
    },
    computed: {
      dones: app => app.todos.filter(todo => todo.done),
      doings: app => app.todos.filter(todo => !todo.done)
    }
  }).$mount('#app')
}

清空代办事项相对于其他操作来说相对比较简单,我们只需要将todos设置为一个全新的数组就可以实现我们想要的效果啦!!!

屏幕录制2022-06-18 02.22.19.gif

2.4 优化用户体验

功能需求: 当用户在无意中输入了一个空格并按下回车后,在页面上生成了对应的代办事项,但很显然这样的代办事项是没有意义的。

图片.png

我们可以通过v-model中的trim修饰符进行去除字符两边的空格,从而优化用户体验:

<header>
  <section>
    <label for="title">{{ title }}</label>
    <input
      type="text"
      id="title"
      name="title"
      ref="input"
      placeholder="添加ToDo"
      required="required"
      autocomplete="off"
      v-model.trim="content"
      @keyup.enter="addToDo"
    />
  </section>
</header>

第三章:利用组件化思想优化代码

通过前两章节的学习我们充分掌握了vue的基本使用,现在我们通过最近学习的组件化思想来优化这一份代码,如果您不知道组件化的思想,请前往官网进行深度学习。

我们在回顾之前所写的代码的时候我们可以看到如下一个代码片段存在很多重复性代码:

<section>
​
  <h2>
    正在进行
    <span id="todocount">{{ doings.length }}</span>
  </h2>
  <ol id="todolist" class="demo-box">
    <li
      v-for="todo in doings"
      :key="todo.id"
    >
      <input type="checkbox" @change="updateState(todo.id, $event)" />
      <p>{{ todo.content }}</p>
      <a href="javascript:;" @click="dropToDo(todo.id)"></a>
    </li>
  </ol>
​
  <h2>
    已经完成
    <span id="donecount">{{ dones.length }}</span>
  </h2>
  <ul id="donelist">
    <li
      v-for="todo in dones"
      :key="todo.id"
    >
      <input type="checkbox" checked @change="updateState(todo.id, $event)" />
      <p>{{ todo.content }}</p>
      <a href="javascript:;" @click="dropToDo(todo.id)"></a>
    </li>
  </ul></section>

针对这种现象,我们可以对他们进行独立的组件封装:

<!-- 封装重复性代码 -->
<template id="todos">
  <div>
    <h2>
      {{ state ? '已经完成' : '正在进行' }}
      <span>{{ todos.length }}</span>
    </h2>
    <ol :id="state ? 'done' : 'doing'">
      <li
        v-for="todo in todos"
        :key="todo.id"
      >
        <input type="checkbox" @change="$emit('update-state', todo.id, $event)" :checked="state" />
        <p>{{ todo.content }}</p>
        <a href="javascript:;" @click="$emit('drop-todo', todo.id)"></a>
      </li>
    </ol>
  </div>
</template><!--  注意:此处是为了方便查看不同提取出来的,实际上section元素的位置不变 --><section>
  <!-- 在页面上使用组件,并传递属性和自定义事件 -->
  <todos
    :todos="doings"
    @update-state="updateState"
    @drop-todo="dropToDo"
  >
  </todos>
  <todos
    state
    :todos="dones"
    @update-state="updateState"
    @drop-todo="dropToDo"
  >
  </todos></section>
​
window.onload = () => {
  const app = new Vue({
    // 注册局部组件
    components: {
      // 注册标签名为 todos 的局部组件
      todos: {
        // 绑定组件模板
        template: '#todos',
        // 接收父组件传递的属性,默认会将这些属性绑定到组件实例上(通俗来说,就是你可以在组件内部通过this去得到它)
        props: {
          // fasle 表示正在进行,true 表示已经完成
          state: {
            type: Boolean,
            default: false
          },
          // 待办事项列表的数据
          todos: {
            type: Array,
            default: []
          }
        }
      }
    },
    data: {
      title: 'ToDoList',
      content: '',
      todos: []
    },
    methods: {
      addToDo () {
        if (this.content) {
          this.todos.unshift({ id: this.todos.length, content: this.content, done: false })
          this.content = ''
          this.$refs.input.blur()
        }
      },
      updateState (id, e) {
        let index
​
        this.todos = this.todos.map((todo, i) => {
          if (todo.id === id) {
            todo.done = e.target.checked
            index = i
          }
          return todo
        })
​
        const target = this.todos.splice(index, 1).at(0)
​
        this.todos.unshift(target)
      },
      dropToDo (id) {
        this.todos = this.todos.filter(todo => todo.id !== id)
      },
      clear () {
        this.todos = []
      }
    },
    computed: {
      dones: app => app.todos.filter(todo => todo.done),
      doings: app => app.todos.filter(todo => !todo.done)
    }
  }).$mount('#app')
}

在完成以上操作后,我们进入到浏览器进行查看效果时,我们会发现效果产生了一点点丢失,这是因为我们在封装组件之前是通过ulol来区分两种不同分组之间的区别的,后面我们统一采用了ol作为分组的根元素导致出现效果丢失。我们可以前往修改css文件,修改如下:

body {
    margin: 0;
    padding: 0;
    font-size: 16px;
    background: #CDCDCD;
}
​
header {
    height: 50px;
    background: #333;
    background: rgba(47, 47, 47, 0.98);
}
​
section {
    margin: 0 auto;
}
​
label {
    float: left;
    width: 100px;
    line-height: 50px;
    color: #DDD;
    font-size: 24px;
    cursor: pointer;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
​
header input {
    float: right;
    width: 60%;
    height: 24px;
    margin-top: 12px;
    text-indent: 10px;
    border-radius: 5px;
    box-shadow: 0 1px 0 rgba(255, 255, 255, 0.24), 0 1px 6px rgba(0, 0, 0, 0.45) inset;
    border: none
}
​
input:focus {
    outline-width: 0
}
​
h2 {
    position: relative;
}
​
span {
    position: absolute;
    top: 2px;
    right: 5px;
    display: inline-block;
    padding: 0 5px;
    height: 20px;
    border-radius: 20px;
    background: #E6E6FA;
    line-height: 22px;
    text-align: center;
    color: #666;
    font-size: 14px;
}
​
ol,
ul {
    padding: 0;
    list-style: none;
}
​
li input {
    position: absolute;
    top: 2px;
    left: 10px;
    width: 22px;
    height: 22px;
    cursor: pointer;
}
​
p {
    margin: 0;
}
​
li p input {
    top: 3px;
    left: 40px;
    width: 70%;
    height: 20px;
    line-height: 14px;
    text-indent: 5px;
    font-size: 14px;
}
​
li {
    height: 32px;
    line-height: 32px;
    background: #fff;
    position: relative;
    margin-bottom: 10px;
    padding: 0 45px;
    border-radius: 3px;
    border-left: 5px solid #629A9C;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07);
}
​
ol li {
    cursor: move;
}
​
ul li {
    border-left: 5px solid #999;
    opacity: 0.5;
}
​
li a {
    position: absolute;
    top: 2px;
    right: 5px;
    display: inline-block;
    width: 14px;
    height: 12px;
    border-radius: 14px;
    border: 6px double #FFF;
    background: #CCC;
    line-height: 14px;
    text-align: center;
    color: #FFF;
    font-weight: bold;
    font-size: 14px;
    cursor: pointer;
}
​
footer {
    color: #666;
    font-size: 14px;
    text-align: center;
}
​
footer a {
    color: #666;
    text-decoration: none;
    color: #999;
}
​
@media screen and (max-device-width: 620px) {
    section {
        width: 96%;
        padding: 0 2%;
    }
}
​
@media screen and (min-width: 620px) {
    section {
        width: 600px;
        padding: 0 10px;
    }
}

通过本章节的学习,我们了解了组件化在项目开发中是如何应用的以及如何实现父子组件间的信息传递。

第四章:实现记录的永久保存

本章节我们学习如何将数据通过localstorage对象进行永久保存。

4.1 JSON

4.1.1 什么是JSON?

JSON 是一种语法,用来序列化对象、数组、数值、字符串、布尔值和 null 。它基于 JavaScript 语法,但与之不同:JavaScript 不是 JSON,JSON 也不是 JavaScript

4.1.2 JSON中的数组和对象

JSON对象

{
  "name": "ralph",
  "age": 21,
  "gender": true
}

JSON数组

[    "HTML",    "CSS",    "JAVASCRIPT",    {      "group_id": 1,      "group_name": "前端",      "nums": 189    }]

4.1.3 JSON对象转JS对象

JSON.parse方法可以用于将一段JSON字符串解析成js对象,其语法格式如下:JSON.parse(JSON)

返回值:

返回值是被转换后的js对象

代码示例:

const arrJson = `[
    "HTML",
    "CSS",
    "JAVASCRIPT",
    {
      "group_id": 1,
      "group_name": "前端",
      "nums": 189
    }
]`
​
const arr = JSON.parse(arrJSON);

4.1.4 JS对象转JSON对象

JSON.stringify方法可以用于将js对象转换为一段JSON字符串,其语法格式如下:JSON.stringify(obj)

返回值:

返回值是被转换后的json字符串

代码示例:

const arr = [
    "HTML",
    "CSS",
    "JAVASCRIPT",
    {
      group_id: 1,
      group_name: "前端",
      nums: 189
    }
]
​
const arrJson = JSON.parse(arr);

4.2 localStorage

只读的localStorage 属性允许你访问一个Document 源(origin)的对象 Storage;存储的数据将保存在浏览器会话中。localStorage 类似 sessionStorage,但其区别在于:存储在 localStorage 的数据可以长期保留;而当页面会话结束——也就是说,当页面被关闭时,存储在 sessionStorage 的数据会被清除 。

另外,localStorage 中的键值对总是以字符串的形式存储。 (需要注意,和 js 对象相比,键值对总是以字符串的形式存储意味着数值类型会自动转化为字符串类型)。

4.2.1 在本地存储添加记录

我们可以通过setItemApi进行将一段数据存储至浏览器会话中,如果键名已存在,则更新其对应的值。其语法格式如下:localStorage.setItem(key, value)

参数:

key

一个字符串,表示要创建或者更新的键名。

value

一个JSON字符串,表示要创建或更新的键名对应的值。

返回值:

undefined

代码示例:

localStorage.setItem('user', 'ralph');
localStorage.setItem('data', '["rickey","anna"]');

4.2.2 获取本地存储中的记录

我们可以通过getItemApi获取本地存储中的记录,其语法格式如下:localStorage.getItem(key)

参数:

key

一个包含键名的字符串。

返回值:

一个字符串,键名对应的值。如果键名不存在于存储中,则返回 null

代码示例:

const user = localStorage.getItem('user');

4.2.3 移除本地存储中的一条记录

我们可以通过removeItemApi移除本地存储中的单条记录,其语法格式如下:localStorage.removeItem(key)

参数:

key

一个包含将要移除键名的字符串。

返回值:

代码示例:

localStorage.removeItem('user');

4.2.4 清空本地存储的记录

我们可以通过clearApi移除本地存储中的所有记录,其语法格式如下:localStorage.clear()

返回值:

代码示例:

localStorage.clear()

4.3 结合JSON和本地存储实现数据的永久保留

为了结构清晰,本人单独将对本地存储的操作分离到了utils.js文件当中,代码如下所示:

// ToDoList-template/js/utils.js
export const get = key => {
  const data = localStorage.getItem(key)
  return data ? JSON.parse(data) : []
}
​
export const set = (key, value) => {
  localStorage.setItem(key, JSON.stringify(value))
}
​
export const drop = (key) => {
  localStorage.removeItem(key)
}
​
export const clear = () => {
  localStorage.clear()
}
​
const local = {
  get,
  set,
  drop,
  clear
}
​
export default local
​
// ToDoList-template/js/main.js// 导入需要用到的方法
import { set, get, clear } from './utils.js'
​
window.onload = () => {
  const app = new Vue({
    // 注册局部组件
    components: {
      todos: {
        template: '#todos',
        props: {
          state: {
            type: Boolean,
            default: false
          },
          todos: {
            type: Array,
            default: []
          }
        }
      }
    },
    data: {
      title: 'ToDoList',
      content: '',
      todos: []
    },
    created () {
      // 在 data 选项被挂载到vue实例之后得到本地存储中的记录并更新到 todos 属性上
      this.todos = get('todos')
    },
    methods: {
      addToDo () {
        if (this.content) {
          this.todos.unshift({ id: this.todos.length, content: this.content, done: false })
          this.content = ''
          this.$refs.input.blur()
        }
      },
      updateState (id, e) {
        let index
​
        this.todos = this.todos.map((todo, i) => {
          if (todo.id === id) {
            todo.done = e.target.checked
            index = i
          }
          return todo
        })
​
        const target = this.todos.splice(index, 1).at(0)
​
        this.todos.unshift(target)
      },
      dropToDo (id) {
        this.todos = this.todos.filter(todo => todo.id !== id)
      },
      // 清除浏览器缓存
      clear () {
        this.todos = []
        clear()
      }
    },
    computed: {
      dones: app => app.todos.filter(todo => todo.done),
      doings: app => app.todos.filter(todo => !todo.done)
    },
    watch: {
      // 深度监听 todos 的值是否发生改变
      todos: {
        handler () {
          // 更新本地存储
          set('todos', this.todos)
        },
        // 开启深度监听
        deep: true
      }
    }
  }).$mount('#app')
}

如上图所示,我们采用了ES6中的模块导入导出语法,所以我们接下来我们需要在main.js被引入时设置一下对应的type属性:

<script src="js/main.js" type="module"></script>

完结撒花!!!