资讯专栏INFORMATION COLUMN

组件化 Todo List 编写笔记

vspiders / 1766人阅读

摘要:前言在学习组件化开发的时候,自己虽然也能编码实现,但如果不做笔记,只是写代码,学习的效果还不够好。为组件准备好数据之后,就可以开始用它了。新增的代码如下上面是组件新增的代码,用户点击按钮之后,会执行该组件内的函数。

前言

在学习 Vue.js 组件化开发 Todo List 的时候,自己虽然也能编码实现,但如果不做笔记,只是写代码,学习的效果还不够好。只有把自己的实现思路记录下来,遇到的问题和解决方法也记录下来,用文字把这个过程梳理清楚,才能对整个项目有更加清晰、准确的认识。

注:该项目通过 vue-cli 搭建,GitHub 上的地址:todo-list。

TodoItem 组件 显示待办事项清单

先写一个最简单的组件,就是用 v-for 指令显示待办事项清单。数据也是用的本地的数据,这样在这一步能够把更多的精力放在学习组件的编写上。

首先,当然是在 components 目录下新建 TodoItem.vue 文件,用来显示待办事项清单,代码如下:



script 中,name 选项定义了组件的名称 TodoItemprops 选项则定义了组件所接收数据的名称 tasks 和类型:数组(Array)。

template 中,则在根元素 ul 内,通过 li 元素显示待办事项的名称 task.title。加了另一条语句 :key="task.id",是因为 Vue 建议在用 v-for 遍历时,为所遍历的每一项提供一个唯一的 key 属性(参考:key)。这一项不加也完全没关系,只不过 vue-cli 附带的 ESLint 会有错误提示,所以我这里就加上了。

另外这里还有个小知识点,Vue 规定组件的 template 中只能有一个根元素,也就是说下面这种写法是会报错的。个人猜测,之所以会有这种规定,也是为了最终渲染出来的 HTML 结构能更加清晰。仔细想想,这个理念也和组件化是相通的,不是嘛?


这个组件最基本的内容已经写好了,接下来就在 App.vue 中引入它。

引入组件之后,当然还要为它提供数据,这样组件才有内容可以显示。这里也有个知识点,组件中的数据对象 data 必须是函数,因为这样能够保证组件实例不会修改同一个数据对象。刚开始写组件的时候可能容易忽略这个知识点,多写几次就记住了。

export default {
  name: "app",
  components: {
    TodoItem
  },
  data() {
    return {
      tasks: [
        {
          id: "6b9a86f6-1d1a-558a-83df-f98d84cd87bd",
          title: "JS",
          content: "Learn JavaScript",
          completed: true,
          createdAt: "2017-08-02"
        },
        {
          id: "1211bb33-a249-5782-bd97-0d5652438476",
          title: "Vue",
          content: "Learn Vue.js and master it!",
          completed: false,
          createdAt: "2018-01-02"
        }
      ]
    };
  }
};

为组件准备好数据之后,就可以开始用它了。组件的基本用法也很简单,按照它的要求提供数据,然后组件就会按照自己设定的样式把数据显示出来。

上面的代码中,调用了 TodoItem 这个组件,并且将父组件中的数据属性 tasks 绑定到 TodoItem 这个组件的 props 选项上。在 :tasks="tasks" 这句代码中,等号前的 tasks 是子组件 TodoItem 中定义的名称,可以近似地理解为“形参”;等号后面的 tasks 则是父组件中的数据属性,可以近似地理解为“实参”。所以这种用法也可以理解成 :形参="实参",希望这种写法能够帮大家更容易地理解组件传入数据的语法。而父组件的数据属性和子组件的 props 选项都用 tasks 这个名称,是为了保持代码上的一致性,刚接触组件的时候可能觉得分不清谁是谁,但是代码写多了之后就能体会到这种写法的好处了,父组件只负责提供数据,子组件只负责使用数据,保持一致的命名,阅读和修改代码的时候就能很容易看出来互相之间的关系。

保存代码,然后在终端中执行 npm run serve,构建工具就会自动编译,然后在浏览器中打开页面,如果能够看到类似下图中的效果,就说明已经写好了一个最简单的组件了,接下来就要丰富这个 Todo List 的各项功能了。

样式改进

要使用 Bootstrap 的样式,首先需要把它的 CSS 文件引入进来,编辑 public 目录下的 index.html 文件,在 head 中加入下面的 CSS。后面需要引入 CSS 或者 JS 的时候,都可以在这里引入。当然了,也可以通过 npm install xxx 指令以后端库的形式引入,不过这样只能引入 JS,没法引入 CSS。不过有一天在火车上撸代码的时候,发现了以后端形式引入库的一个便利之处,就是它一旦安装好了,没有网络的情况下也完全可以正常用。



  
    
  

接下来就是搭框架,先修改 App.vue,确定整体框架:

在根 div 中加上 class="container",这样子元素就可以应用 col-md-8 这样的网格样式了。然后在子元素中加上 class="col-md-8 offset-md-2 mt-5"col-md-8 表示待办事项占12列宽度的网格中的8列,offset-md-2 表示往右偏移2列之后显示待办事项,这样就能够居中显示了。mt-5 则表示待办事项距离上方有一定空白,留白了才好看。

每个待办事项要显示标题、内容、日期,可以用 Bootstrap 的 Custom Content 列表。

观察上图对应的代码可以知道,a 标签内的 h5 标签可用于显示待办事项的标题,相邻的 small 标签可用于显示时间,a 标签内最后的 small 标签则可用显示于事项的具体内容,因此 TodoItem.vue 组件中可以改成如下内容。

在浏览器中看看页面效果,怎么样,还不错吧?

从服务器获取数据

在实际业务中,数据都是放在服务器上,往往会在前端页面加载完成之后,再向服务器请求数据。这样前后端分离,让前端页面只关注界面部分,数据由后端负责提供,将前后端解耦,就降低了互相之间的依赖性。

要向服务器请求数据,可以用 axios 这个库,和前面引入 Bootstrap 的 CSS 一样,编辑 public 目录下的 index.html 文件,将 axios 这个库的链接加进来。



  
    
  

然后再编辑父组件 App.vue,将数据属性 tasks 的初始值设置为空数组,在 Vue 实例的 created 这个生命周期钩子中获取数据。数据方面参考一个简单的 JSON 存储服务这篇文章的建议 ,放在 myjson 上。

const tasksUrl = "https://api.myjson.com/bins/xxxxx";

export default {
  name: "app",
  components: {
    TodoItem
  },
  data() {
    return {
      tasks: []
    };
  },
  methods: {
    fetchData(jsonUrl, obj) {
      axios
        .get(jsonUrl)
        .then(response => response.data)
        .then(data => {
          data.forEach(ele => {
            obj.push(ele);
          });
        })
        .catch(console.log());
    },
  },
  created() {
    this.fetchData(tasksUrl, this.tasks);
  }
};

从上面的代码可以看到,数据属性的值保存在 tasksUrl 这个 URL 中,通过 axios 获取数据。在 Vue 中更新数组,需要用特定的变异方法,才能触发视图的更新,也就是上面代码中的 obj.push(ele)

另外,上面将更新数据部分的代码抽离成一个多带带的函数 fetchData,这样能够提高代码的可读性。否则如果 created 这个钩子中需要执行五六个操作的时候,把具体的代码全放到这里面,那代码就乱得没法看了。

v-cloak 优化加载体验

为了优化用户体验,可以用 v-cloak 指令,实现组件在数据加载完成之后才显示的功能。

具体的测试结果,可以看视频:http://7xq4gx.com1.z0.glb.clouddn.com/v-cloak_fast-3g.mp4。

在上面这个视频中,通过 Chrome 开发者工具将网速限制为 "Fast 3G" 模式,以便更清楚地展示这个过程。然后点击刷新按钮加载页面,能够看到页面在成功获取到服务器上的数据之后,才会渲染组件内容并显示出来,在这之前页面则一直是空白状态。

TodoMenu 组件 显示菜单按钮

前面知道怎么用组件显示待办事项清单了,那么显示一个菜单列表也很容易了,照葫芦画瓢就行。

首先在父组件 App.vue 中准备数据 menus

export default {
  name: "app",
  components: {
    TodoItem,
    TodoMenu
  },
  data() {
    return {
      tasks: [],
      menus: [
        { tag: "all", text: "全部" },
        { tag: "doing", text: "未完成" },
        { tag: "done", text: "已完成" }
      ]
    };
  }
}

然后选择按钮的样式,自己选用了 Outline buttons,组件代码如下:



与之前编写 TodoItem 组件时相比,代码上主要的区别在于 props 的定义更加详细了,理由参见 Vue.js 官方文档中的风格指南:Prop 定义。

下面是当前的页面效果:

样式改进

基本的功能做出来了,接着来调整一下 TodoMenu 组件的样式,让它更好看一些。

首先是要给按钮之间加上间距,也是前面提到过的留白,就跟设计 PPT 一样,把页面塞得满满的其实很难看。查看 Bootstrap 的文档 Margin and padding,知道了可以用 mr-x 这样的类来设置右边距,测试了几个值之后,最终确定为 mr-2

然后还要给上面的一排按钮和下面的待办事项清单之间也加上间距,这里就用 mb-3 设置按钮的下边距,之前在 TodoItem 组件中设置的 mt-5 则删掉。

现在的页面效果就是这个样子的了:

网页加载完成后突出显示第一个按钮

查看 Bootstrap 的文档可以知道,给按钮添加一个 active 类,按钮就会处于被点击的状态。这样一来,只需要修改 menus 的数据结构,给每个对象添加一个名为 active 的布尔型变量,然后给 TodoMenu 组件动态绑定 active 类,就能实现页面加载完成后突出显示第一个按钮的功能了。

// App.vue
menus: [
  { tag: "all", text: "全部", active: true },
  { tag: "doing", text: "未完成", active: false },
  { tag: "done", text: "已完成", active: false }
]

突出当前被点击的按钮

除了要在网页加载完成后突出显示第一个按钮,还需要在用户点击各个按钮之后,突出显示用户所点击的按钮,这样能够让用户很清楚地看到自己所选中的是哪个按钮。

实现这个需求的流程如下(用了库 ramda):

查找 menusactive 属性为 true 的对象,也就是之前被点击的按钮对应的数据。

查找 menus 中当前被点击的按钮对应的对象:这个需要在子组件 TodoMenu.vue 中触发事件,将被点击的按钮所对应的数据(menu.tag)传递给父组件 App.vue,然后在父组件中查找该数据所对应的对象,如果和第一次查找的对象相同,说明前后两次点击了同一个按钮,那么就不用重复操作了。否则就需要把前一次点击的按钮的 active 属性设置为 false,然后将当前被点击的按钮的 active 属性设置为 true,这样就能够突出显示被点击的按钮了。

新增的代码如下:



  



上面是组件 TodoMenu.vue 新增的代码,用户点击按钮之后,会执行该组件内的 activeButton 函数。在函数中会触发 active 事件,并将当前按钮所对应对象的 tag 属性的值传给父组件。




而上面的这段代码则是父组件 App.vue 中新增的代码,父组件监听到了子组件触发的 active 事件,就会执行父组件中的 activeButton 函数,对比两次点击的是否为同一按钮,然后根据结果执行对应的操作:如果点击的是不同的按钮,则将之前所点击的按钮对应的对象属性 active 值设置为 false,并将当前点击的按钮对应的对象属性的 active 的值设置为 true,Vue 监听到对象属性的变化,从而将类名动态绑定到 HTML 标签上,实现按钮的突出显示。

PS:自己之前的实现方案,是通过 jQuery 先将 menus 中所有对象的 active 属性设置为 false,然后用原生 JS 将触发了监听事件对象的 active 属性设置为 true,虽然代码也很简洁,但是代码的逻辑还是不如用 ramda 这个库的实现方式清晰。

点击按钮显示对应事项

这个需求可以在上一个需求的流程里完成,就是页面加载完成时,显示全部的待办事项;之后每次用户点击按钮,和前一次突出显示的按钮进行对比,如果相同,说明显示的还是那些待办事项,自然不用做什么操作;如果不同,那就显示按钮所对应分类的待办事项。

export default {
  data() {
    return {
      currTag: ""
    }
  },
  computed: {
    filteredTasks() {
      if (this.currTag === "all") {
        return JSON.parse(JSON.stringify(this.tasks));
      } else if (this.currTag === "doing") {
        return R.filter(task => task.completed === false)(this.tasks);
      } else if (this.currTag === "done") {
        return R.filter(task => task.completed === true)(this.tasks);
      } else {
        return null;
      }
    }
  },
  methods: {
    fetchData(jsonUrl, obj) {
      axios
        .get(jsonUrl)
        .then(response => response.data)
        .then(data => {
          data.forEach(ele => {
            obj.push(ele);
          });
        })
        .then((this.currTag = "all"))
        .catch(console.log());
    }
  }
}

在上面的代码中,通过字符串属性 currTag 标记当前所点击的按钮,计算属性 filteredTaks 则根据 currTag 的值筛选出所要显示的待办事项。而在 fetchData 方法中,新增的 .then((this.currTag = "all")) 会在获取到数据之后设置所要显示的事项类别,这样整个流程就完整了。

上面这些只是功能上的变动,在界面部分也要对应调整,这样才能有更好的用户体验。具体来说,就是对于已完成的待办事项,复选框应为选中状态,并且文字的颜色要淡一些,这样才能和未完成的待办事项区分开来。

而实际的代码其实很简单,就是将传入组件的数据与 HTML 元素动态绑定:





下面是调整好界面之后的效果图:

Todo Edit 组件 点击待办事项后显示编辑界面

首先设计编辑界面的基本样式,在这里用的是 Bootstrap 中的 Card 这个组件,这样可以把内部的元素都包裹到 card 中。待办事项的标题和内容显示在 textarea 元素中,待办事项的创建时间则显示在 card-footer 中。这个组件的代码如下所示:




从上面的代码中可以看到,将 idtitletextareatask.title 属性进行了双向绑定,idcontenttextarea 则与 task.content 属性进行了双向绑定,分别用来显示待办事项的标题和内容。

在父组件 App.vue 中,对象类型的数据属性 currTask 保存子组件 TodoEdit.vue 中所要显示的待办事项,并通过布尔类型的计算属性 renderEdit 决定是否要渲染子组件 TodoEdit.vue。在用户还没有点击待办事项的时候,还不需要渲染编辑界面,数据属性 currTask 还是个空对象,计算属性 renderEditfalse。在用户点击了某个待办事项之后,需要在编辑界面中显示数据属性 currTask 中的内容,计算属性 renderEdittrue,这样才会渲染子组件 TodoEdit.vue

父组件 App.vue 中新增的代码如下所示:




从上面的代码可以看到,在页面及数据加载完成之后,用户点击待办事项之前,不会显示编辑界面。用户点击待办事项之后,将当前事项的信息保存至数据属性 currTask 中,计算属性 renderEdit 此时的值也为 true,便会渲染子组件 TodoEdit.vue,并将数据属性 currTask 的内容显示在子组件中。

完成之后的效果如下图所示:

样式改进

按照上面的方法完善代码之后,现在可以显示待办事项的编辑界面了。但是点击待办事项的话,浏览器地址栏中的地址会在最后附加上一个 # 字符:http://localhost:8080/#。如果不想有这种变化,那么就可以去掉 TodoItem.vue 组件的 href 属性,然后设置鼠标悬浮至该组件的 a 标签时显示手型指针即可:

此外,由于 TodoEdit.vue 组件中,显示待办事项标题和内容用的都是 textarea 标签,而这个标签是可以通过拖动其右下角的标记来改变其大小的。但是对于待办事项而言,标题的文字数量一般都不多,不希望改变其大小,那么就要为这个标签进行多带带的设置,设置其 resize 属性为 none 即可。

此时的效果如下所示:

切换编辑界面的显示状态

这个功能所要实现的效果,就是用户连续多次点击同一个待办事项时,编辑界面会在显示/隐藏两种状态之间来回切换,给用户以更好的使用体验。

最开始的思路:

新建数据属性 prevId 用于保存用户上一次点击的待办事项的 id 属性,并且将用户本次点击的待办事项的 id 属性与之进行对比。

如果两个属性值不同,说明前后两次点击的是不同的待办事项,就不需要隐藏编辑界面,并且将用户本次所点击的待办事项的 id 属性保存在 prevId 中,这样用户下一次再点击待办事项,就能与更新后的 prevId 属性进行对比。

如果两个属性值相同,说明两次点击的是同一个待办事项,那么 prevId 属性就没必要更新了,同时要切换编辑界面的显示状态。

从前面的代码可以知道,计算属性 renderEdit 的值决定了是否要渲染组件 TodoEdit.vue,数据属性 currTask 非空就会渲染。而用户首次点击待办事项之后,currTask 就永远都是非空的了,也就意味着编辑界面一直会被渲染。而这里需要实现的功能,是要让这个组件在显示/隐藏两种状态之间来回切换,需要注意的是,组件的“渲染”和“显示”是两回事,被渲染出来的组件,可以通过设置其 display 这个 CSS 属性的值为 false 来把它隐藏了。那么 Vue.js 中有没有类似的方式实现这个功能呢?当然有!那就是 v-show 指令。该指令后跟的表达式只要为真值,就会显示该元素,否则就会隐藏该元素。这不刚好就是我们需要的功能吗?这样一来,就可以通过优化代码逻辑,让上面新建的数据属性 prevId 来完成两件事:一方面这个数据属性可以用来保存每次点击的待办事项的 id 属性,另一方面还可以用它来决定是否要显示编辑界面。啊哈,一举两得,是不是很爽?另外,prevId 这个名称只是表示了它最原始的含义,其实可以重命名为 showEdit,用来表示它最终的业务逻辑,这样在阅读代码的时候就更容易理解了。下面就是优化后的代码逻辑:

页面加载完成,showEdit 为空。

首次点击某个事项,保存 idshowEdit

之后再点击同一个事项,idshowEdit 相同,则清空 showEdit

点击不同的事项,idshowEdit 不同,则更新至 showEdit 中。

流程已经很清楚了,代码自然也是水到渠成:



俗话说优化无止境,上面的 editTask 方法中新增的代码,其实还可以进一步优化,不知道你有没有想到该如何优化呢?快动手试试吧!

点击“保存”按钮,保存更改 Header 组件 添加 Header 及文本内容 添加 Icon Font Footer 组件 添加固定在底部的 Footer 参考资料

Collapsible contents (code block) in comments / spoiler tag · Issue #166 · dear-github/dear-github:用 Markdown 语法,实现内容的折叠效果。不过最后呈现出来的效果不好,就没有用上。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92949.html

相关文章

  • react入门笔记

    摘要:是程序的入口文件自动化测试定义组件扩大点击区域虚拟的生成数据模板数据模板生成虚拟虚拟就是一个对象,用它来描述真实损耗了性能用虚拟的结构生成真实的来显示发生变化数据模板生成新的虚拟极大提升了性能数据更新比较原始虚拟和新的虚拟的区别找到区别极大 Index.js是程序的入口文件 PWA progressive web application(registerServiceWorker) A...

    Hanks10100 评论0 收藏0
  • Vue学习笔记之一 - 入门

    摘要:而在页面中,在之内的元素只需写一个。但是元素的内容被更改之后,控件中的内容并不会同步更新。下面的代码,在中遍历实例中属性里的每一项,并将每个与绑定。而在定义组件的代码中,接收传入的,并在元素中显示中的字符串。 URL:Introduction - Vue.js 注意 所演示的示例,都是在JS中将Vue实例绑定至HTML中的指定元素,然后再通过Vue实例中data内的属性或者method...

    BLUE 评论0 收藏0
  • Vue学习笔记之一 - 入门

    摘要:而在页面中,在之内的元素只需写一个。但是元素的内容被更改之后,控件中的内容并不会同步更新。下面的代码,在中遍历实例中属性里的每一项,并将每个与绑定。而在定义组件的代码中,接收传入的,并在元素中显示中的字符串。 URL:Introduction - Vue.js 注意 所演示的示例,都是在JS中将Vue实例绑定至HTML中的指定元素,然后再通过Vue实例中data内的属性或者method...

    Ververica 评论0 收藏0
  • 实现一个稍微复杂的simplelist

    摘要:是一个专门为应用所设计的集中式状态管理架构。此时可以帮助我们实现状态的管理。每个任务都归属于一个清单,有唯一的清单。说到这,一个复杂的的基本结构和功能已经出现了。 使用过一些清单类的应用程序,像 WunderList, Google Keep等,用来记录一些计划和安排,也试着将自己的计划安排同笔记一起整理在 Evernote 中,但是无论哪种方式用起来总觉得少了点什么,如果两者的一些功...

    solocoder 评论0 收藏0
  • 前端单元测试探索

    摘要:单元测试的首要目的不是为了能够编写出大覆盖率的全部通过的测试代码,而是需要从使用者调用者的角度出发,尝试函数逻辑的各种可能性,进而辅助性增强代码质量测试是手段而不是目的。 本文已发布在稀土掘金 转载请注明原文链接:https://github.com/ecmadao/Co... 虽然很多公司有自己的测试部门,而且前端开发大多不涉及测试环节,但鉴于目前前端领域的快速发展,其涉及面越来...

    陈江龙 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<