webpack+vue从零开始打造todoList2
本章节开始todoList项目实现。
项目开始前的配置,包括入口,出口,打包,创建实例挂载等,前文已经介绍完成。这里不重复了,也可以直接安装脚手架开始项目功能。
首先展示出源文件的目录结构:
|-- src, |-- App.vue, |-- main.js, |-- assets, | |-- images, | | |-- bg.jpg, | | |-- Checked.svg, | | |-- unChecked.svg, | |-- styles, | |-- global.styl, | |-- reset.css, | |-- test.css, |-- components, |-- MainFooter.vue, |-- MainHeader.vue, |-- MainTodo, |-- MainTodo.vue, |-- _coms, |-- TodoInfo.vue, |-- TodoItem.vue,
一 。项目拆分
- MainHeader 组件:页头
- MainTodo 组件:功能
- TodoItem 组件:输入内容后添加到列表上
- TodoInfo 组件:按钮功能(已完成查看,未完成查看,清除)
- MainFooter 组件:页脚
二。MainHeader
MainHeader.vue
<template> <header class="main-header"> <h1>TodoList</h1> </header> </template> <script> export default { name:'MainHeader' } </script> <style lang="stylus" scoped> .main-header text-align :center h1 margin :20px font-size: 100px font-weight: 300 color: rgb(252,157,154) text-shadow: 5px 5px 5px rgba(0,0,0,0.3) </style>
App.vue
<template> <div> <main-header></main-header> </div> </template> <script> import './assets/styles/global.styl' import MainHeader from './components/MainHeader.vue' export default { name:'App', components: { //组件名:组件对象 MainHeader: MainHeader, } } </script> <style lang="stylus" scoped> </style>
优化:一些会反复用到的样式,建议提取出来做个文件
在 styles 目录下,创建 theme.styl, 将主题配色定义成变量
$red = rgb(254, 67, 101)
$lightred = rgb(252, 157, 154)
$yellow = rgb(249, 205, 173)
$green = rgb(131, 175, 155)
$lightgreen = rgb(200, 200, 169)
1.在需要用到的时候, 引入相关的文件
修改 MainHeader.vue
<style lang="stylus" scoped>
@import '../assets/styles/theme.styl'
...
h1
color: $lightred
</style>
2.加入别名
在 webpack 的配置中, 可以指定别名
resolve: { alias: { 'vue': 'vue/dist/vue.js', '@': path.resolve(__dirname, '../src'), 'styles': path.resolve(__dirname, '../src/assets/styles'),
'images': path.resolve(__dirname, '../src/assets/images'), } },
stylus引入修改 :@import '~styles/theme.styl'
~ 被看做模块间的依赖
三。MainTodo
- MainTodo -- input输入框
- TodoItem 子组件 列表
- TodoInfo 子组件 选择按钮
App.vue组件挂载,参考上面。
1. MainTodo.vue
<template> <div class="main-todo"> <input type="text" class="add-todo" placeholder="what to do?" autofocus /> </div> </template> <script> export default { name:'MainTodo'//组件命名尽量和文件名统一 } </script> <style lang="stylus" scoped> .main-todo margin: 0 auto width: 600px background-color: #fff box-shadow: 0 0 5px #666 .add-todo padding: 16px 16px 16px 36px width: 100% font-size: 24px font-family: inherit font-weight: inherit //继承 color: inherit border: none outline: none box-sizing: border-box //盒子模型 </style>
2. TodoItem页面实现
子组件挂载到父组件MainTodo.vue 上
<template> <div class="main-todo"> <input type="text" class="add-todo" placeholder="what to do?" autofocus /> <todo-item></todo-item> </div> </template> <script> import TodoItem from './_coms/TodoItem.vue' export default { name:'MainTodo', components: { TodoItem } } </script> ...
TodoItem.vue 页面样式
<template> <div class="todo-item"> <input type="checkbox" /> <label>todo1</label> <button></button> </div> </template> <script> export default{ name:'TodoItem' } </script> <style lang="stylus" scoped> @import '~styles/theme.styl' .todo-item padding : 10px display :flex //弹性盒子 justify-content :space-between //两端对齐 align-items :center //垂直居中 font-size 24px &:hover button:after content 'x' font-size 24px color $lightred cursor pointer &.completed label color #d9d9d9 text-decoration line-through label flex:1 //该元素占据剩下全部空间 transition :color .4s //过渡效果 input width 50px height 30px text-align center border none outline none appearance none //清除input默认样式 &:after //&:父元素选择器 content url('~images/unchecked1.svg') &:checked:after content url('~images/check1.svg') button width 40px background-color transparent border none outline none appearance none </style>
---- 包含三个元素:选择框,label,button删除按钮
---- 实现基本样式:元素之间两端对齐,垂直居中;选择框、button 按钮清除原有样式;input设置显示矢量图,因层级较多,调用引入别名;button显示文本‘ x ’表示删除。
优化:在 styles 文件夹下创建 mixins.styl, 将一些通用样式封装成函数 (mixins)。
mixins.styl
cleanDefaultStyle()
appearance: none
border: none
outline: none
'{ }' , ' ; ' 这些标点我都省略了,这是因为我的 vs code插件stylus配置过了,打包生成时都会自动添加。
使用:
<style lang="stylus" scoped> @import '~styles/theme.styl' @import '~styles/mixins.styl' ... input width: 50px height: 30px text-align: center cleanDefaultStyle() ... </style>
3 TodoItem 业务实现
需求分析:
- 添加功能
- 选中功能
- 删除功能
1)添加功能
- 输入内容, 按下键盘回车键时, 添加一条待办记录
- 如果没有输入内容, 不添加
- 添加后, 之前的内容清空
核心点:父组件要向子组件进行传值
MainTodo.vue
<template> <div class="main-todo"> <input type="text" class="add-todo" placeholder="what to do?" autofocus v-model="content" @keyup.enter="addTodo"/> <todo-item v-for="(item,index) in todoData" :key="index" :todo="item"></todo-item> </div> </template> <script> import TodoItem from './_coms/TodoItem.vue' let i= 0 export default { name:'MainTodo', components: { TodoItem }, data(){ return{ todoData:[ // { // id:0, // content: 'todo1', // completed: false // }, // { // id: 1, // content: 'todo2', // completed: false // }添加功能完成,清空测试数据 ], content:'' } }, methods:{ addTodo(){ if(this.content === '') return this.todoData.unshift({ id:i++, //id: 2 content:this.content, completed:false }) this.content = '' } } } </script> <style lang="stylus" scoped> .main-todo margin: 0 auto width: 600px background-color: #fff box-shadow: 0 0 5px #666 .add-todo padding: 16px 16px 16px 36px width: 100% font-size: 24px font-family: inherit font-weight: inherit color: inherit border: none outline: none box-sizing: border-box </style>
=>
定义一个todoData数据对象,添加测试数据,在todo-item组件上循环遍历,这时候页面展示的数据还是"todo1"测试数据,要绑定一个:todo="item"获取数据对象传递给子组件。
定义数据content,用v-model使content和input文本双向绑定,添加一个键盘enter事件操作:
当content为空,返回return;否则向数据对象中添加数据unshift,清空content 。
=>
TodoItem修改显示从父组件接收过来的数据
<template> <div class="todo-item"> <input type="checkbox" /> <label>{{todo.content}}</label> <button></button> </div> </template> <script> export default{ name:'TodoItem', props:{ todo: Object //获取父组件传递的object类型的todo数据 } } </script>
2)选中功能
- 点击选中按钮, 按钮变为选中样式, 并且文本显示被删除(有删除线)
- 再次点击选中按钮, 按钮变为末选中样式, 并且文本正常显示(没有删除线)
核心点:样式绑定
TodoItem.vue
<template> <!-- <div class="todo-item"> --> <div :class="['todo-item',todo.completed ? 'completed': '']"> <input type="checkbox" v-model="todo.completed" /> <label>{{todo.content}}</label> <button></button> </div> </template>
- 前面我们已经写好了input的选中样式和label的删除样式(类completed)。
- 给div添加多种样式绑定,通过父组件传递过来的completed的值判断是否添加类。
- todo.completed默认值是false,可以和input数据绑定,当input选中后,.completed的值即为true。
3)删除功能
- 点击删除按钮时, 删除这条待办记录
核心点:子组件向父组件传值
TodoItem.vue
<template> <div :class="['todo-item',todo.completed ? 'completed': '']"> <input type="checkbox" v-model="todo.completed" /> <label>{{todo.content}}</label> <button @click="delItem"></button> </div> </template> <script> export default{ name:'TodoItem', props:{ todo: Object }, methods:{ delItem(){ this.$emit('del',this.todo.id) } } } </script> ...
=> 给button按钮添加一个点击事件delItem,在点击事件中我们通过$emit去监听父组件的事件del,并传递参数todo.id.
MainTodo.vue
<template> <div class="main-todo"> <input type="text" class="add-todo" placeholder="what to do?" autofocus v-model="content" @keyup.enter="addTodo"/> <todo-item v-for="(item,index) in todoData" :key="index" :todo="item" @del="handleDeleteItem"></todo-item> </div> </template> <script> import TodoItem from './_coms/TodoItem.vue' let i= 0 export default { name:'MainTodo', components: { TodoItem }, data(){ return{ todoData:[], content:'' } }, methods:{ addTodo(){ if(this.content === '') return this.todoData.unshift({ id:i++, //id: 2 content:this.content, completed:false }) this.content = '' }, handleDeleteItem(id){ this.todoData.splice(this.todoData.findIndex(item=> item.id === id),1) } } } </script> ...
=> 给组件todo-item 添加 del 事件,使用handleDeleteItem方法并接收参数id,在del事件中使用findIndex遍历数据获取到当前id对应的索引值,使用splice删除该条数据。
4.TodoInfo页面实现
- 总计
- tab 选项卡
- 清除框
在 MainTodo.vue 中引入 TodoInfo 组件,参考上面TodoItem。
TodoInfo.vue
<template> <div class="todo-info"> <span class="total">1 item left</span> <div class="tabs"> <a v-for="(item, index) in states" :key="index">{{ item }}</a> </div> <button class="clear">Clear Completed</button> </div> </template> <script> export default { name: 'TodoInfo', data() { return { states: ['all', 'active', 'completed'] } }, } </script> <style lang="stylus" scoped> @import '~styles/theme.styl' .todo-info display flex justify-content space-between font-weight 400 padding 5px 10px line-height 30px border-top 1px solid rgba(0,0,0,0.1) .total color $red .tabs display flex justify-content space-between width 200px a border 1px solid $lightred padding 0 10px border-radius 5px &.actived background-color $lightred color #fff .clear padding 0 10px background-color $green border-radius 5px color #fff appearance none border none outline none </style>
优化代码,公用样式提取封装,语义化标签
mixins.styl
cleanDefaultStyle(){ appearance: none border: none outline: none } btn(c, border = false){ padding: 0 10px border-radius: 5px cursor: pointer cleanDefaultStyle() if (border == true) border: 1px solid c else background-color: c color: #fff } primaryBtn() btn(rgb(252, 157, 154)) primaryBorderBtn() btn(rgb(252, 157, 154), true) infoBtn() btn(rgb(131, 175, 155))
TodoInfo.vue
<template> <div class="todo-info"> <span class="total">1 item left</span> <div class="tabs"> <a class="btn primary border" v-for="(item, index) in states" :key="index">{{ item }}</a> </div> <button class="btn info">Clear Completed</button> </div> </template> <script> export default { name: 'TodoInfo', data() { return { states: ['all', 'active', 'completed'] } }, } </script> <style lang="stylus" scoped> @import '~styles/theme.styl' @import '~styles/mixins.styl' .todo-info display flex justify-content space-between font-weight 400 padding 5px 10px line-height 30px border-top 1px solid rgba(0,0,0,0.1) .total color $red .tabs display flex justify-content space-between width 200px .btn.primary.border primaryBorderBtn() &.actived primaryBtn() .btn.info infoBtn() </style>
5.TodoInfo业务实现
- 统计功能
- 切换显示不同状态功能
- 删除功能
1)统计功能
- 实时统计剩余的待办事项
核心点:监听器watch
MainTodo.vue
<template> <div class="main-todo"> ... <todo-info :total="total"></todo-info> </div> </template> <script> ... data(){ return{ todoData:[], content:'', total:0 } }, methods:{ ... }, watch:{ todoData:{ deep:true, handler(){ this.total= this.todoData.filter(item=> item.completed == false).length } } } } </script> ...
- deep: true -- 表示监听每个一个属性的变化
- handler-- 处理函数, 在此是为了过滤没有完成的数组, 并获取长度
通过监听todoData数据,在函数中获取到属性为completed= false 的数组的总数,绑定数据 :total='total',把数据传递给子组件。
注意:前面的total是子组件要使用的变量,引号内的是父组件要传递的变量
TodoInfo.vue
... <span class="total">{{total}} item left</span> ... <script> ... props:{ total: Number }, ... </script>
props接收数据,类型是number,显示页面上
2) 切换功能
- 点击不同的状态分别显示不同状态的待办事项
- 点击 all, 显示所有的待办事项
- 点击 active, 显示未完成的待办事项
- 点击 completed, 显示已完成的待办事项
核心点:计算属性computed
TodoInfo.vue
<template> <div class="todo-info"> <span class="total">{{total}} item left</span> <div class="tabs"> <a :class="['btn','primary','border', state == item ? 'actived' : '']" v-for="(item, index) in states" :key="index" @click="toggleState(item)">{{ item }}</a> </div> <button class="btn info">Clear Completed</button> </div> </template> <script> export default { name: 'TodoInfo', props:{ total: Number }, data() { return { states: ['all', 'active', 'completed'], state:'all' } }, methods:{ toggleState(item){ this.state = item, this.$emit('toggleState',this.state) } } } </script>
选中样式绑定,这里就不在重复了。
添加一个点击事件,通过$emit监听父组件的事件toggleState,并传递当前的item值
MainTodo.vue
<template> <div class="main-todo"> <input type="text" class="add-todo" placeholder="what to do?" autofocus v-model="content" @keyup.enter="addTodo"/> <todo-item v-for="(item,index) in filterData" :key="index" :todo="item" @del="handleDeleteItem"></todo-item> <todo-info :total="total" @toggleState="handleToggleState"></todo-info> </div> </template> <script> import TodoInfo from './_coms/TodoInfo.vue' import TodoItem from './_coms/TodoItem.vue' let i= 0 export default { name:'MainTodo',//组件命名尽量和文件名统一 components: { TodoItem, TodoInfo, TodoInfo, }, data(){ return{ todoData:[], content:'', total:0, filter:'all' } }, methods:{ addTodo(){ if(this.content === '') return this.todoData.unshift({ id:i++, //id: 2 content:this.content, completed:false }) this.content = '' }, handleDeleteItem(id){ this.todoData.splice(this.todoData.findIndex(item=> item.id === id),1) }, handleToggleState(state){ this.filter = state } }, computed:{ filterData(){ switch (this.filter){ case 'all': return this.todoData break case 'active': return this.todoData.filter(item => item.completed == false) break case 'completed': return this.todoData.filter(item => item.completed == true) break } } }, watch:{ todoData:{ deep:true, handler(){ this.total= this.todoData.filter(item=> item.completed == false).length } } } } </script>
在父组件定义事件toggleState,绑定handleToggleState方法获取到传递过来的参数。在计算属性中定义filterData方法返回todoDate筛选后的数据,并通过v-for来遍历它,达到选中切换效果。
3)删除功能
需求:点击clear删除所有已完成的事项
=>参考上面选中方法:定义一个点击事件,通过$emit监听执行父组件的删除事件,不用传值,直接筛选出未完成的数组赋值给todoData
TodoInfo.vue
<button class="btn info" @click='clearCompleted'>Clear Completed</button>
methods:{ clearCompleted(){ this.$emit('clearCompleted') } }
MainTodo.vue
<todo-info :total="total" @toggleState="handleToggleState" @clearCompleted="handleClear"></todo-info>
handleClear(){
this.todoData = this.todoData.filter(item => item.completed == false)
}
四。MainFooter
版权所有,是纯文本内容,不需要业务代码,编写好组件内容,挂载到vue即可。
MainFooter.vue
<template> <footer class="main-footer">Written By Name</footer> </template> <style lang="stylus" scoped> .main-footer margin: 20px auto text-align: center color: #fff text-shadow: 5px 5px 5px #000 </style>
App.vue
<template> <div> <main-header></main-header> <main-todo></main-todo> <main-footer></main-footer> </div> </template> <script> import './assets/styles/global.styl' import MainHeader from './components/MainHeader.vue' import MainTodo from './components/MainTodo/MainTodo.vue' import MainFooter from './components/MainFooter.vue' export default { name:'App', components: { //组件名:组件对象 MainHeader: MainHeader, MainTodo, MainFooter } } </script> <style lang="stylus" scoped> </style>
------------------------------------------------- end --------------------------------------------------