02-组件化编程与Vu额 Click脚手架

1.Vue组件化编程(只有1个数字是一级标题)

1.1 模块与组件、模块化与组件化(两个数字组成是二级标题)

1.1.1模块(三个数字是三级标题 依次类推)

  1. 理解:向外提供特定功能的 js 程序,一般就是一个 js 文件
  2. 为什么:js 文件很多很复杂
  3. 作用:复用 js,简化 js 的编写,提高 js 运行效率

1.1.2. 组件

  1. 定义:用来实现局部功能的代码和资源的集合(html/css/js/image...)
  2. 为什么:一个界面的功能很复杂
  3. 作用:复用编码,简化项目编码,提高运行效率

1.1.3. 模块化

当应用中的 js 都以模块来编写的,那这个应用就是一个模块化的应用

1.1.4. 组件化

当应用中的功能都是多组件的方式来编写的,那这个应用就是一个组件化的应用

2.2. 非单文件组件

2.2.1. 基本使用

html 复制代码
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>基本使用</title>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<div id="root">
			<h1>{{msg}}</h1>
			<hr>
			<!-- 第三步:编写组件标签 -->
			<school></school>
			<hr>
			<!-- 第三步:编写组件标签 -->
			<student></student>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false

		//第一步:创建school组件
		const school = Vue.extend({
            //组件定义时,一定不要写el配置项,因为最终所有的组件都要被一个vm管理,由vm决定服务于哪个容器。
			template:`
				<div class="demo">
					<h2>学校名称:{{schoolName}}</h2>
					<h2>学校地址:{{address}}</h2>	
				</div>
			`,
			data(){
				return {
					schoolName:'尚硅谷',
					address:'北京昌平'
				}
			}
		})

		//第一步:创建student组件
		const student = Vue.extend({
			template:`
				<div>
					<h2>学生姓名:{{studentName}}</h2>
					<h2>学生年龄:{{age}}</h2>
				</div>
			`,
			data(){
				return {
					studentName:'JOJO',
					age:20
				}
			}
		})
		
		//创建vm
		new Vue({
			el:'#root',
			data:{
				msg:'你好,JOJO!'
			},
			//第二步:注册组件(局部注册)
			components:{
				school,
				student
			}
		})
	</script>
</html>

总结:

  • Vue中使用组件的三大步骤:
  1. 定义组件(创建组件)
  2. 注册组件
  3. 使用组件(写组件标签)
  • 如何定义一个组件?

使用Vue.extend(options)创建,其中options和new Vue(options)时传入的options几乎一样,但也有点区别:

1.el不要写,为什么?

最终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器

2.data必须写成函数,为什么?

避免组件被复用时,数据存在引用关系

  • 如何注册组件?
  1. 局部注册:new Vue的时候传入components选项
  2. 全局注册:Vue.component('组件名',组件)
  • 编写组件标签:<school></school>

2.2.2. 组件注意事项

html 复制代码
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>组件注意事项</title>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<div id="root">
			<h1>{{msg}}</h1>
			<school></school>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false
		
		const school = Vue.extend({
			name:'atguigu',
			template:`
				<div>
					<h2>学校名称:{{name}}</h2>	
					<h2>学校地址:{{address}}</h2>	
				</div>
			`,
			data(){
				return {
					name:'尚硅谷',
					address:'北京'
				}
			}
		})

		new Vue({
			el:'#root',
			data:{
				msg:'欢迎学习Vue!'
			},
			components:{
				school
			}
		})
	</script>
</html>
总结:

关于组件名:

  • 一个单词组成:
  1. 第一种写法(首字母小写):school
  2. 第二种写法(首字母大写):School
  • 多个单词组成:
  1. 第一种写法(kebab-case命名):my-school
  2. 第二种写法(CamelCase命名):MySchool (需要Vue脚手架支持)
  • 备注:
  1. 组件名尽可能回避HTML中已有的元素名称,例如:h2、H2都不行
  2. 可以使用name配置项指定组件在开发者工具中呈现的名字
  • 关于组件标签:
  1. 第一种写法:<school></school>
  2. 第二种写法:<school/>
  3. 备注:不使用脚手架时,<school/>会导致后续组件不能渲染
  • 一个简写方式:const school = Vue.extend(options)可简写为:const school = options
2.2.3. 组件的嵌套
html 复制代码
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>组件的嵌套</title>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<div id="root">
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false
		
		//定义student组件
		const student = Vue.extend({
			template:`
				<div>
					<h2>学生名称:{{name}}</h2>	
					<h2>学生年龄:{{age}}</h2>	
				</div>
			`,
			data(){
				return {
					name:'JOJO',
					age:20
				}
			}
		})

		//定义school组件
		const school = Vue.extend({
			template:`
				<div>
					<h2>学校名称:{{name}}</h2>	
					<h2>学校地址:{{address}}</h2>	
					<student></student>
				</div>
			`,
			components:{
				student
			},
			data(){
				return {
					name:'尚硅谷',
					address:'北京'
				}
			}
		})

		//定义hello组件
		const hello = Vue.extend({
			template:`
				<h1>{{msg}}</h1>
			`,
			data(){
				return {
					msg:"欢迎学习尚硅谷Vue教程!"
				}
			}
		})

		//定义app组件
		const app = Vue.extend({
			template:`
				<div>
					<hello></hello>
					<school></school>
				</div>
			`,
			components:{
				school,
				hello
			}
		})

		//创建vm
		new Vue({
			template:`
				<app></app>
			`,
			el:'#root',
			components:{
				app
			}
		})
	</script>
</html>
2.2.4. VueComponent

关于VueComponent:

  1. school组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的
  2. 我们只需要写<school/>或<school></school>,Vue解析时会帮我们创建school组件的实例对象,即Vue帮我们执行的:new VueComponent(options)
  3. 特别注意:每次调用Vue.extend,返回的都是一个全新的VueComponent!
  4. 关于this指向:
  • 组件配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是VueComponent实例对象
  • new Vue(options)配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是Vue实例对象

5.VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)

Vue的实例对象,以后简称vm

只有在本笔记中VueComponent的实例对象才简称为vc

2.2.5. 一个重要的内置关系
html 复制代码
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>一个重要的内置关系</title>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<div id="root">
			<school></school>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false
		Vue.prototype.x = 99

		const school = Vue.extend({
			name:'school',
			template:`
				<div>
					<h2>学校名称:{{name}}</h2>	
					<h2>学校地址:{{address}}</h2>	
					<button @click="showX">点我输出x</button>
				</div>
			`,
			data(){
				return {
					name:'尚硅谷',
					address:'北京'
				}
			},
			methods: {
				showX(){
					console.log(this.x)
				}
			},
		})

		const vm = new Vue({
			el:'#root',
			data:{
				msg:'你好'
			},
			components:{school}
		})
	</script>
</html>
  1. 一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype
  2. 为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue 原型上的属性、方法
2.3. 单文件组件
  • School.vue:
html 复制代码
<template>
    <div id='Demo'>
        <h2>学校名称:{{name}}</h2>
        <h2>学校地址:{{address}}</h2>
        <button @click="showName">点我提示学校名</button>
    </div>
</template>

<script>
    export default {
        name:'School',
        data() {
            return {
                name:'尚硅谷',
                address:'北京'
            }
        },
        methods: {
            showName(){
                alert(this.name)
            }
        },
    }
</script>

<style>
    #Demo{
        background: orange;
    }
</style>
  • Student.vue:
html 复制代码
<template>
    <div>
        <h2>学生姓名:{{name}}</h2>
        <h2>学生年龄:{{age}}</h2>
    </div>
</template>

<script>
    export default {
        name:'Student',
        data() {
            return {
                name:'JOJO',
                age:20
            }
        },
    }
</script>
  • App.vue:
html 复制代码
<template>
    <div>
        <School></School>
        <Student></Student>
    </div>
</template>

<script>
    import School from './School.vue'
    import Student from './Student.vue'

    export default {
        name:'App',
        components:{
            School,
            Student
        }
    }
</script>
  • main.js:
html 复制代码
import App from './App.vue'

new Vue({
    template:`<App></App>`,
    el:'#root',
    components:{App}
})
  • index.html
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>单文件组件练习</title>
</head>
<body>
    <div id="root"></div>
    <script src="../../js/vue.js"></script>
    <script src="./main.js"></script>
</body>
</html>

3. 使用Vue CLI脚手架

3.1. 初始化脚手架
3.1.1. 说明
  1. Vue 脚手架是 Vue 官方提供的标准化开发工具(开发平台)
  2. 最新的版本是 4.x
  3. 文档:Vue CLI
3.1.2. 具体步骤
  1. 如果下载缓慢请配置 npm 淘宝镜像:npm config set registry http://registry.npm.taobao.org
  2. 全局安装@vue/cli:npm install -g @vue/cli
  3. 切换到你要创建项目的目录,然后使用命令创建项目:vue create xxxx
  4. 选择使用vue的版本
  5. 启动项目:npm run serve
  6. 暂停项目:Ctrl+C

3.1.3. 分析脚手架结构

脚手架文件结构:

html 复制代码
.文件目录
├── node_modules 
├── public
│   ├── favicon.ico: 页签图标
│   └── index.html: 主页面
├── src
│   ├── assets: 存放静态资源
│   │   └── logo.png
│   │── component: 存放组件
│   │   └── HelloWorld.vue
│   │── App.vue: 汇总所有组件
│   └── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── package.json: 应用包配置文件 
├── README.md: 应用描述文件
└── package-lock.json: 包版本控制文件

src/components/School.vue:

html 复制代码
<template>
    <div id='Demo'>
        <h2>学校名称:{{name}}</h2>
        <h2>学校地址:{{address}}</h2>
        <button @click="showName">点我提示学校名</button>
    </div>
</template>

<script>
    export default {
        name:'School',
        data() {
            return {
                name:'尚硅谷',
                address:'北京'
            }
        },
        methods: {
            showName() {
                alert(this.name)
            }
        },
    }
</script>

<style>
    #Demo{
        background: orange;
    }
</style>

src/components/Student.vue:

html 复制代码
<template>
    <div>
        <h2>学生姓名:{{name}}</h2>
        <h2>学生年龄:{{age}}</h2>
    </div>
</template>

<script>
    export default {
        name:'Student',
        data() {
            return {
                name:'JOJO',
                age:20
            }
        },
    }
</script>

src/App.vue:

html 复制代码
<template>
    <div>
        <School></School>
        <Student></Student>
    </div>
</template>

<script>
    import School from './components/School.vue'
    import Student from './components/Student.vue'

    export default {
        name:'App',
        components:{
            School,
            Student
        }
    }
</script>

src/main.js:

html 复制代码
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
    el:'#app',
    render: h => h(App),
})

public/index.html:

html 复制代码
<!DOCTYPE html>
<html lang="">
    <head>
        <meta charset="UTF-8">
        <!-- 针对IE浏览器的特殊配置,含义是让IE浏览器以最高渲染级别渲染页面 -->
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <!-- 开启移动端的理想端口 -->
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <!-- 配置页签图标 -->
        <link rel="icon" href="<%= BASE_URL %>favicon.ico">
        <!-- 配置网页标题 -->
        <title><%= htmlWebpackPlugin.options.title %></title>
    </head>
    <body>
        <!-- 容器 -->
        <div id="app"></div>
    </body>
</html>

3.1.4. render函数

html 复制代码
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
    el:'#app',
    // 简写形式
	render: h => h(App),
    // 完整形式
	// render(createElement){
	//     return createElement(App)
	// }
})

总结:

关于不同版本的函数:

1.vue.js 与 vue.runtime.xxx.js的区别:

  1. vue.js 是完整版的 Vue,包含:核心功能+模板解析器

  2. vue.runtime.xxx.js 是运行版的 Vue,只包含核心功能,没有模板解析器

2.因为 vue.runtime.xxx.js 没有模板解析器,所以不能使用 template 配置项,需要使用 render函数接收到的createElement 函数去指定具体内容

3.1.5. 修改默认配置

  • vue.config.js 是一个可选的配置文件,如果项目的(和 package.json 同级的)根目录中存在这个文件,那么它会被 @vue/cli-service 自动加载
  • 使用 vue.config.js 可以对脚手架进行个性化定制,详见配置参考 | Vue CLI
html 复制代码
module.exports = {
    pages: {
        index: {
            // 入口
            entry: 'src/index/main.js'
        }
    },
  // 关闭语法检查
  lineOnSave:false
}

                            

3.2. ref属性

html 复制代码
<template>
    <div>
        <h1 ref="title">{{msg}}</h1>
        <School ref="sch"/>
        <button @click="show" ref="btn">点我输出ref</button>
    </div>
</template>

<script>
    import School from './components/School.vue'
    export default {
        name:'App',
        components: { School },
        data() {
            return {
                msg:'欢迎学习Vue!'
            }
        },
        methods:{
            show(){
                console.log(this.$refs.title)
                console.log(this.$refs.sch)
                console.log(this.$refs.btn)
            }
        }
    }
</script>
------------------------------------------------

                           

总结:

ref属性:

  1. 被用来给元素或子组件注册引用信息(id的替代者)
  2. 应用在html标签上获取的是真实DOM元素,应用在组件标签上获取的是组件实例对象(vc)
  3. 使用方式:

1.打标识:<h1 ref="xxx"></h1> 或 <School ref="xxx"></School>

2.获取:this.$refs.xxx

3.3. props配置项

src/App.vue:

html 复制代码
<template>
    <div>
        <Student name="JOJO" sex="男酮" :age="20" />
    </div>
</template>

<script>
    import Student from './components/Student.vue'
    export default {
        name:'App',
        components: { Student },
    }
------------------------------------------------

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                        
原文链接:https://blog.csdn.net/qq_55593227/article/details/119717498

src/components/Student.vue:

html 复制代码
<template>
    <div>
        <h1>{{msg}}</h1>
        <h2>学生姓名:{{name}}</h2>
        <h2>学生性别:{{sex}}</h2>
        <h2>学生年龄:{{age}}</h2>     
    </div>
</template>

<script>
    export default {
        name:'Student',
        data() {
            return {
                msg:"我是一名来自枝江大学的男酮,嘿嘿,我的金轮~~",
            }
        },
        // 简单声明接收
		// props:['name','age','sex']

        // 接收的同时对数据进行类型限制
		/* props:{
			name:String,
			age:Number,
			sex:String
		} */

        // 接收的同时对数据进行类型限制 + 指定默认值 + 限制必要性
		props:{
			name:{
				type:String,
				required:true,
			},
			age:{
				type:Number,
				default:99
			},
			sex:{
				type:String,
				required:true
			}
		}
    }
</script>


                            

总结:

props配置项:

  1. 功能:让组件接收外部传过来的数据

  2. 传递数据:<Demo name="xxx"/>

  3. 接收数据:

  • 1.第一种方式(只接收):props:['name']

  • 2.第二种方式(限制数据类型):props:{name:String}

  • 3.第三种方式(限制类型、限制必要性、指定默认值):

html 复制代码
props:{
    name:{
    	type:String, //类型
        required:true, //必要性
        default:'JOJO' //默认值
    }
}

3.4. mixin混入

局部混入:

src/mixin.js:

html 复制代码
export const mixin = {
    methods: {
        showName() {
            alert(this.name)
        }
    },
    mounted() {
        console.log("你好呀~")
    }
}

src/components/School.vue

html 复制代码
<template>
    <div>
        <h2 @click="showName">学校姓名:{{name}}</h2>
        <h2>学校地址:{{address}}</h2>   
    </div>
</template>

<script>
    //引入混入
    import {mixin} from '../mixin'
    
    export default {
        name:'School',
        data() {
            return {
                name:'尚硅谷',
				address:'北京'
            }
        },
        mixins:[mixin]
    }
</script>

src/components/Student.vue:

html 复制代码
<template>
    <div>
        <h2 @click="showName">学生姓名:{{name}}</h2>
        <h2>学生性别:{{sex}}</h2>   
    </div>
</template>

<script>
    //引入混入
    import {mixin} from '../mixin'
    
    export default {
        name:'Student',
        data() {
            return {
                name:'JOJO',
				sex:'男'
            }
        },
		mixins:[mixin]
    }
</script>

src/App.vue:

html 复制代码
<template>
    <div>
        <School/>
        <hr/>
        <Student/>
    </div>
</template>

<script>
    import Student from './components/Student.vue'
    import School from './components/School.vue'

    export default {
        name:'App',
        components: { Student,School },
    }
</script>

全局混入:

src/main.js:

html 复制代码
import Vue from 'vue'
import App from './App.vue'
import {mixin} from './mixin'

Vue.config.productionTip = false
Vue.mixin(mixin)

new Vue({
    el:"#app",
    render: h => h(App)
})

总结:

mixin(混入):

  1. 功能:可以把多个组件共用的配置提取成一个混入对象

  2. 使用方式:

1.定义混入:

html 复制代码
const mixin = {
    data(){....},
    methods:{....}
    ....
}

2.使用混入:

1.全局混入:Vue.mixin(xxx)

2.局部混入:mixins:['xxx']

3.备注:

1.组件和混入对象含有同名选项时,这些选项将以恰当的方式进行"合并",在发生冲突时以组件优先。

复制代码
var mixin = {
	data: function () {
		return {
    		message: 'hello',
            foo: 'abc'
    	}
  	}
}

new Vue({
  	mixins: [mixin],
  	data () {
    	return {
      		message: 'goodbye',
            bar: 'def'
    	}
    },
  	created () {
    	console.log(this.$data)
    	// => { message: "goodbye", foo: "abc", bar: "def" }
  	}
})

2.同名生命周期钩子将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。

复制代码
var mixin = {
  	created () {
    	console.log('混入对象的钩子被调用')
  	}
}

new Vue({
  	mixins: [mixin],
  	created () {
    	console.log('组件钩子被调用')
  	}
})

// => "混入对象的钩子被调用"
// => "组件钩子被调用"

3.5. plugin插件

src/plugin.js:

复制代码
export default {
	install(Vue,x,y,z){
		console.log(x,y,z)
		//全局过滤器
		Vue.filter('mySlice',function(value){
			return value.slice(0,4)
		})

		//定义混入
		Vue.mixin({
			data() {
				return {
					x:100,
					y:200
				}
			},
		})

		//给Vue原型上添加一个方法(vm和vc就都能用了)
		Vue.prototype.hello = ()=>{alert('你好啊')}
	}
}

src/main.js:

复制代码
import Vue from 'vue'
import App from './App.vue'
import plugin from './plugin'

Vue.config.productionTip = false
Vue.use(plugin,1,2,3)

new Vue({
    el:"#app",
    render: h => h(App)
})

src/components/School.vue:

复制代码
<template>
    <div>
        <h2>学校姓名:{{name | mySlice}}</h2>
        <h2>学校地址:{{address}}</h2>   
    </div>
</template>

<script>
    export default {
        name:'School',
        data() {
            return {
                name:'尚硅谷atguigu',
				address:'北京'
            }
        }
    }
</script>

src/components/Student.vue:

复制代码
<template>
    <div>
        <h2>学生姓名:{{name}}</h2>
        <h2>学生性别:{{sex}}</h2> 
        <button @click="test">点我测试hello方法</button>  
    </div>
</template>

<script>
    export default {
        name:'Student',
        data() {
            return {
                name:'JOJO',
				sex:'男'
            }
        },
        methods:{
            test() {
                this.hello()
            }
        }
    }
</script>

总结:

插件:

  1. 功能:用于增强Vue

  2. 本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据

  3. 定义插件:

    plugin.install = function (Vue, options) {
    // 1. 添加全局过滤器
    Vue.filter(....)

    复制代码
         // 2. 添加全局指令
         Vue.directive(....)
     
         // 3. 配置全局混入
         Vue.mixin(....)
     
         // 4. 添加实例方法
         Vue.prototype.$myMethod = function () {...}
         Vue.prototype.$myProperty = xxxx
     }

4.使用插件:Vue.use(plugin)

3.6. scoped样式

src/components/School.vue:

复制代码
<template>
    <div class="demo">
        <h2>学校姓名:{{name}}</h2>
        <h2>学校地址:{{address}}</h2>   
    </div>
</template>

<script>
    export default {
        name:'School',
        data() {
            return {
                name:'尚硅谷',
				address:'北京'
            }
        }
    }
</script>

<style scoped>
    .demo{
        background-color: blueviolet;
    }
</style>

src/components/Student.vue

复制代码
<template>
    <div class="demo">
        <h2>学生姓名:{{name}}</h2>
        <h2>学生性别:{{sex}}</h2> 
    </div>
</template>

<script>
    export default {
        name:'Student',
        data() {
            return {
                name:'JOJO',
				sex:'男'
            }
        }
    }
</script>

<style scoped>
    .demo{
        background-color: chartreuse;
    }
</style>

src/App.vue:

复制代码
<template>
    <div>
        <School/>
        <Student/>
    </div>
</template>

<script>
    import Student from './components/Student.vue'
    import School from './components/School.vue'

    export default {
        name:'App',
        components: { Student,School },
    }
</script>

总结:

scoped样式:

  1. 作用:让样式在局部生效,防止冲突
  2. 写法:<style scoped>

3.7. Todo-List案例

src/components/MyHeader.vue:

复制代码
<template>
    <div class="todo-header">
        <input type="text" placeholder="请输入你的任务名称,按回车键确认" @keydown.enter="add" v-model="title"/>
    </div>
</template>

<script>
    import {nanoid} from 'nanoid'
    export default {
        name:'MyHeader',
        data() {
            return {
                title:''
            }
        },
        methods:{
            add(){
                if(!this.title.trim()) return
                const todoObj = {id:nanoid(),title:this.title,done:false}
                this.addTodo(todoObj)
                this.title = ''
            }
        },
        props:['addTodo']
    }
</script>

<style scoped>
    .todo-header input {
        width: 560px;
        height: 28px;
        font-size: 14px;
        border: 1px solid #ccc;
        border-radius: 4px;
        padding: 4px 7px;
    }

    .todo-header input:focus {
        outline: none;
        border-color: rgba(82, 168, 236, 0.8);
        box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
    }
</style>

src/components/MyItem.vue:

复制代码
<template>
    <li>
        <label>
            <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>
            <span>{{todo.title}}</span>
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id,todo.title)">删除</button>
    </li>
</template>

<script>
    export default {
        name:'MyItem',
        props:['todo','checkTodo','deleteTodo'],
        methods:{
            handleCheck(id){
                this.checkTodo(id)
            },
            handleDelete(id,title){
                if(confirm("确定删除任务:"+title+"吗?")){
                    this.deleteTodo(id)
                }
            }
        }
    }
</script>

<style scoped>
    li {
        list-style: none;
        height: 36px;
        line-height: 36px;
        padding: 0 5px;
        border-bottom: 1px solid #ddd;
    }

    li label {
        float: left;
        cursor: pointer;
    }

    li label li input {
        vertical-align: middle;
        margin-right: 6px;
        position: relative;
        top: -1px;
    }

    li button {
        float: right;
        display: none;
        margin-top: 3px;
    }

    li:before {
        content: initial;
    }

    li:last-child {
        border-bottom: none;
    }

    li:hover {
        background-color: #eee;
    }

    li:hover button{
        display: block;
    }
</style>

src/components/MyList.vue:

复制代码
<template>
    <ul class="todo-main">
        <MyItem 
            v-for="todo in todos" 
            :key="todo.id" 
            :todo="todo" 
            :checkTodo="checkTodo"
            :deleteTodo="deleteTodo"
        />
    </ul>
</template>

<script>
    import MyItem from './MyItem.vue'

    export default {
        name:'MyList',
        components:{MyItem},
        props:['todos','checkTodo','deleteTodo']
    }
</script>

<style scoped>
    .todo-main {
        margin-left: 0px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding: 0px;
    }

    .todo-empty {
        height: 40px;
        line-height: 40px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding-left: 5px;
        margin-top: 10px;
    }
</style>

src/components/MyFooter.vue:

复制代码
<template>
    <div class="todo-footer" v-show="total">
        <label>
            <input type="checkbox" v-model="isAll"/>
        </label>
        <span>
            <span>已完成{{doneTotal}}</span> / 全部{{total}}
        </span>
        <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
    </div>
</template>

<script>
    export default {
        name:'MyFooter',
        props:['todos','checkAllTodo','clearAllTodo'],
        computed:{
            doneTotal(){
                return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
            },
            total(){
                return this.todos.length
            },
            isAll:{
                get(){
                    return this.total === this.doneTotal && this.total > 0
                },
                set(value){
                    this.checkAllTodo(value)
                }
            }
        },
        methods:{
            clearAll(){
                this.clearAllTodo()
            }
        }
    }
</script>

<style scoped>
    .todo-footer {
        height: 40px;
        line-height: 40px;
        padding-left: 6px;
        margin-top: 5px;
        }

    .todo-footer label {
        display: inline-block;
        margin-right: 20px;
        cursor: pointer;
    }

    .todo-footer label input {
        position: relative;
        top: -1px;
        vertical-align: middle;
        margin-right: 5px;
    }

    .todo-footer button {
        float: right;
        margin-top: 5px;
    }
</style>

src/App.vue:

复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
            <MyHeader :addTodo="addTodo"/>
            <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
            <MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>
            </div>
        </div>
    </div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import MyList from './components/MyList.vue'
    import MyFooter from './components/MyFooter.vue'

    export default {
        name:'App',
        components: { MyHeader,MyList,MyFooter },
        data() {
            return {
                todos:[
                    {id:'001',title:'抽烟',done:false},
                    {id:'002',title:'喝酒',done:false},
                    {id:'003',title:'烫头',done:false},
                ]
            }
        },
        methods:{
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkTodo(id){
                this.todos.forEach((todo)=>{
                    if(todo.id === id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(id){
                this.todos = this.todos.filter(todo => todo.id !== id)
            },
            //全选or取消勾选
            checkAllTodo(done){
                this.todos.forEach(todo => todo.done = done)
            },
            //删除已完成的todo
            clearAllTodo(){
                this.todos = this.todos.filter(todo => !todo.done)
            }
        }
    }
</script>

<style>
    body {
    	background: #fff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        font-size: 14px;
        line-height: 20px;
        text-align: center;
        vertical-align: middle;
        cursor: pointer;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #fff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #fff;
        background-color: #bd362f;
    }

    .btn:focus {
    	outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }
    .todo-container .todo-wrap {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

效果:

总结:

  • 组件化编码流程:
  1. 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突
  2. 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:

1.一个组件在用:放在组件自身即可

  1. 一些组件在用:放在他们共同的父组件上(状态提升)

3.实现交互:从绑定事件开始

  • props适用于:
  1. 父组件 ==> 子组件 通信
  2. 子组件 ==> 父组件 通信(要求父组件先给子组件一个函数)
  • 使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的

  • props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做

3.8. WebStorage

复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>localStorage</title>
</head>
<body>
    <h2>localStorage</h2>
    <button onclick="saveDate()">点我保存数据</button><br/>
    <button onclick="readDate()">点我读数据</button><br/>
    <button onclick="deleteDate()">点我删除数据</button><br/>
    <button onclick="deleteAllDate()">点我清空数据</button><br/>

    <script>
        let person = {name:"JOJO",age:20}

        function saveDate(){
            localStorage.setItem('msg','localStorage')
            localStorage.setItem('person',JSON.stringify(person))
        }
        function readDate(){
            console.log(localStorage.getItem('msg'))
            const person = localStorage.getItem('person')
            console.log(JSON.parse(person))
        }
        function deleteDate(){
            localStorage.removeItem('msg')
            localStorage.removeItem('person')
        }
        function deleteAllDate(){
            localStorage.clear()
        }
    </script>
</body>
</html>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>sessionStorage</title>
</head>
<body>
    <h2>sessionStorage</h2>
    <button onclick="saveDate()">点我保存数据</button><br/>
    <button onclick="readDate()">点我读数据</button><br/>
    <button onclick="deleteDate()">点我删除数据</button><br/>
    <button onclick="deleteAllDate()">点我清空数据</button><br/>

    <script>
        let person = {name:"JOJO",age:20}

        function saveDate(){
            sessionStorage.setItem('msg','sessionStorage')
            sessionStorage.setItem('person',JSON.stringify(person))
        }
        function readDate(){
            console.log(sessionStorage.getItem('msg'))
            const person = sessionStorage.getItem('person')
            console.log(JSON.parse(person))
        }
        function deleteDate(){
            sessionStorage.removeItem('msg')
            sessionStorage.removeItem('person')
        }
        function deleteAllDate(){
            sessionStorage.clear()
        }
    </script>
</body>
</html>

总结:

  1. 存储内容大小一般支持5MB左右(不同浏览器可能还不一样)

  2. 浏览器端通过Window.sessionStorageWindow.localStorage属性来实现本地存储机制

  3. 相关API:

1.xxxStorage.setItem('key', 'value'):该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值

  1. xxxStorage.getItem('key'):该方法接受一个键名作为参数,返回键名对应的值

  2. xxxStorage.removeItem('key'):该方法接受一个键名作为参数,并把该键名从存储中删除

4.xxxStorage.clear():该方法会清空存储中的所有数据

4.备注:

1.SessionStorage存储的内容会随着浏览器窗口关闭而消失

2.LocalStorage存储的内容,需要手动清除才会消失

3.xxxStorage.getItem(xxx)如果 xxx 对应的 value 获取不到,那么getItem()的返回值是null

4.JSON.parse(null)的结果依然是null

使用本地存储优化Todo-List:

src/App.vue:

复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
            <MyHeader :addTodo="addTodo"/>
            <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
            <MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>
            </div>
        </div>
    </div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import MyList from './components/MyList.vue'
    import MyFooter from './components/MyFooter.vue'

    export default {
        name:'App',
        components: { MyHeader,MyList,MyFooter },
        data() {
            return {
                //若localStorage中存有'todos'则从localStorage中取出,否则初始为空数组
                todos:JSON.parse(localStorage.getItem('todos')) || []
            }
        },
        methods:{
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkTodo(id){
                this.todos.forEach((todo)=>{
                    if(todo.id === id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(id){
                this.todos = this.todos.filter(todo => todo.id !== id)
            },
            //全选or取消勾选
            checkAllTodo(done){
                this.todos.forEach(todo => todo.done = done)
            },
            //删除已完成的todo
            clearAllTodo(){
                this.todos = this.todos.filter(todo => !todo.done)
            }
        },
        watch:{
            todos:{
                //由于todos是对象数组,所以必须开启深度监视才能发现数组中对象的变化
                deep:true,
                handler(value){
                    localStorage.setItem('todos',JSON.stringify(value))
                }
            }
        }
    }
</script>

<style>
    body {
    	background: #fff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        font-size: 14px;
        line-height: 20px;
        text-align: center;
        vertical-align: middle;
        cursor: pointer;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #fff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #fff;
        background-color: #bd362f;
    }

    .btn:focus {
    	outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }
    .todo-container .todo-wrap {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

3.9. 自定义事件

3.9.1. 绑定

src/App.vue:

复制代码
<template>
    <div class="app">
        <!-- 通过父组件给子组件传递函数类型的props实现子给父传递数据 -->
        <School :getSchoolName="getSchoolName"/>

        <!-- 通过父组件给子组件绑定一个自定义事件实现子给父传递数据(第一种写法,使用@或v-on) -->
        <!-- <Student @jojo="getStudentName"/> -->

        <!-- 通过父组件给子组件绑定一个自定义事件实现子给父传递数据(第二种写法,使用ref) -->
		<Student ref="student"/>
    </div>
</template>

<script>
    import Student from './components/Student.vue'
    import School from './components/School.vue'

    export default {
        name:'App',
        components: { Student,School },
        methods:{
            getSchoolName(name){
                console.log("已收到学校的名称:"+name)
            },
            getStudentName(name){
                console.log("已收到学生的姓名:"+name)      
            }
        },
        mounted(){
            this.$refs.student.$on('jojo',this.getStudentName)
        }
    }
</script>


<style scoped>
	.app{
		background-color: gray;
		padding: 5px;
	}
</style>

src/components/Student.vue:

复制代码
<template>
    <div class="student">
        <h2>学生姓名:{{name}}</h2>
        <h2>学生性别:{{sex}}</h2>
        <button @click="sendStudentName">点我传递学生姓名</button> 
    </div>
</template>

<script>
    export default {
        name:'Student',
        data() {
            return {
                name:'JOJO',
				sex:'男'
            }
        },
        methods:{
            sendStudentName(){
                this.$emit('jojo',this.name)
            }
        }
    }
</script>

<style scoped>
    .student{
        background-color: chartreuse;
        padding: 5px;
		margin-top: 30px;
    }
</style>
------------------------------------------------

3.9.2. 解绑

src/App.vue:

复制代码
<template>
    <div class="app">
        <Student @jojo="getStudentName"/>
    </div>
</template>

<script>
    import Student from './components/Student.vue'

    export default {
        name:'App',
        components: { Student },
        methods:{
            getStudentName(name){
                console.log("已收到学生的姓名:"+name)      
            }
        }
    }
</script>

<style scoped>
	.app{
		background-color: gray;
		padding: 5px;
	}
</style>

src/components/Student.vue:

复制代码
<template>
    <div class="student">
        <h2>学生姓名:{{name}}</h2>
        <h2>学生性别:{{sex}}</h2>
        <button @click="sendStudentName">点我传递学生姓名</button> 
        <button @click="unbind">解绑自定义事件</button> 
    </div>
</template>

<script>
    export default {
        name:'Student',
        data() {
            return {
                name:'JOJO',
				sex:'男'
            }
        },
        methods:{
            sendStudentName(){
                this.$emit('jojo',this.name)
            },
            unbind(){
                // 解绑一个自定义事件
                // this.$off('jojo')
                // 解绑多个自定义事件
                // this.$off(['jojo'])
                // 解绑所有自定义事件
                this.$off()
            }
        }
    }
</script>

<style scoped>
    .student{
        background-color: chartreuse;
        padding: 5px;
		margin-top: 30px;
    }
</style>

总结:

组件的自定义事件:

  1. 一种组件间通信的方式,适用于:==子组件 > 父组件

  2. 使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)

  3. 绑定自定义事件:

1.第一种方式,在父组件中:<Demo @atguigu="test"/><Demo von:atguigu="test"/>

2.第二种方式,在父组件中:

复制代码
<Demo ref="demo"/>
...
mounted(){
    this.$refs.demo.$on('atguigu',data)
}

3.若想让自定义事件只能触发一次,可以使用once修饰符,或$once方法

4.触发自定义事件:this.$emit('atguigu',数据)

5.解绑自定义事件:this.$off('atguigu')

6.组件上也可以绑定原生DOM事件,需要使用native修饰符

7.注意:通过this.$refs.xxx.$on('atguigu',回调)绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,否则this指向会出问题!

使用自定义事件优化Todo-List:

src/App.vue:

复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
            <MyHeader @addTodo="addTodo"/>
            <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
            <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
            </div>
        </div>
    </div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import MyList from './components/MyList.vue'
    import MyFooter from './components/MyFooter.vue'

    export default {
        name:'App',
        components: { MyHeader,MyList,MyFooter },
        data() {
            return {
                todos:JSON.parse(localStorage.getItem('todos')) || []
            }
        },
        methods:{
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkTodo(id){
                this.todos.forEach((todo)=>{
                    if(todo.id === id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(id){
                this.todos = this.todos.filter(todo => todo.id !== id)
            },
            //全选or取消勾选
            checkAllTodo(done){
                this.todos.forEach(todo => todo.done = done)
            },
            //删除已完成的todo
            clearAllTodo(){
                this.todos = this.todos.filter(todo => !todo.done)
            }
        },
        watch:{
            todos:{
                deep:true,
                handler(value){
                    localStorage.setItem('todos',JSON.stringify(value))
                }
            }
        }
    }
</script>

<style>
    body {
    	background: #fff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        font-size: 14px;
        line-height: 20px;
        text-align: center;
        vertical-align: middle;
        cursor: pointer;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #fff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #fff;
        background-color: #bd362f;
    }

    .btn:focus {
   		outline: none;
    }

    .todo-container {
    	width: 600px;
    	margin: 0 auto;
    }
    .todo-container .todo-wrap {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

src/components/MyHeader.vue:

复制代码
<template>
    <div class="todo-header">
        <input type="text" placeholder="请输入你的任务名称,按回车键确认" @keydown.enter="add" v-model="title"/>
    </div>
</template>

<script>
    import {nanoid} from 'nanoid'
    export default {
        name:'MyHeader',
        data() {
            return {
                title:''
            }
        },
        methods:{
            add(){
                if(!this.title.trim()) return
                const todoObj = {id:nanoid(),title:this.title,done:false}
                this.$emit('addTodo',todoObj)
                this.title = ''
            }
        }
    }
</script>

<style scoped>
    /*header*/
    .todo-header input {
        width: 560px;
        height: 28px;
        font-size: 14px;
        border: 1px solid #ccc;
        border-radius: 4px;
        padding: 4px 7px;
    }

    .todo-header input:focus {
        outline: none;
        border-color: rgba(82, 168, 236, 0.8);
        box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
    }
</style>

src/components/MyFooter:

复制代码
<template>
    <div class="todo-footer" v-show="total">
        <label>
            <input type="checkbox" v-model="isAll"/>
        </label>
        <span>
            <span>已完成{{doneTotal}}</span> / 全部{{total}}
        </span>
        <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
    </div>
</template>

<script>
    export default {
        name:'MyFooter',
        props:['todos'],
        computed:{
            doneTotal(){
                return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
            },
            total(){
                return this.todos.length
            },
            isAll:{
                get(){
                    return this.total === this.doneTotal && this.total > 0
                },
                set(value){
                    this.$emit('checkAllTodo',value)
                }
            }
        },
        methods:{
            clearAll(){
                this.$emit('clearAllTodo')
            }
        }
    }
</script>

<style scoped>
    .todo-footer {
        height: 40px;
        line-height: 40px;
        padding-left: 6px;
        margin-top: 5px;
        }

    .todo-footer label {
        display: inline-block;
        margin-right: 20px;
        cursor: pointer;
    }

    .todo-footer label input {
        position: relative;
        top: -1px;
        vertical-align: middle;
        margin-right: 5px;
    }

    .todo-footer button {
        float: right;
        margin-top: 5px;
    }
</style>

3.10. 全局事件总线

src/main.js:

复制代码
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
	el:'#app',
	render: h => h(App),
	beforeCreate() {
		Vue.prototype.$bus = this //安装全局事件总线
	}
})

src/App.vue:

复制代码
<template>
	<div class="app">
		<School/>
		<Student/>
	</div>
</template>

<script>
	import Student from './components/Student'
	import School from './components/School'

	export default {
		name:'App',
		components:{School,Student}
	}
</script>

<style scoped>
	.app{
		background-color: gray;
		padding: 5px;
	}
</style>

src/components/School.vue:

复制代码
<template>
	<div class="school">
		<h2>学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
	</div>
</template>

<script>
	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷',
				address:'北京',
			}
		},
		methods:{
			demo(data) {
				console.log('我是School组件,收到了数据:',data)
			}
		},
		mounted() {
			this.$bus.$on('demo',this.demo)
		},
		beforeDestroy() {
			this.$bus.$off('demo')
		},
	}
</script>

<style scoped>
	.school{
		background-color: skyblue;
		padding: 5px;
	}
</style>

src/components/Student.vue:

复制代码
<template>
	<div class="student">
		<h2>学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
		<button @click="sendStudentName">把学生名给School组件</button>
	</div>
</template>

<script>
	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男'
			}
		},
		methods: {
			sendStudentName(){
				this.$bus.$emit('demo',this.name)
			}
		}
	}
</script>

<style scoped>
	.student{
		background-color: pink;
		padding: 5px;
		margin-top: 30px;
	}
</style>

总结:

全局事件总线(GlobalEventBus):

  1. 一种组件间通信的方式,适用于任意组件间通信

  2. 安装全局事件总线:

    new Vue({
    ...
    beforeCreate() {
    Vue.prototype.bus = this //安装全局事件总线,bus就是当前应用的vm
    },
    ...
    })

3.使用事件总线:

1.接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件 自身

复制代码
export default {
    methods(){
        demo(data){...}
    }
    ...
    mounted() {
        this.$bus.$on('xxx',this.demo)
    }
}

2.提供数据:this.$bus.$emit('xxx',data)

4.最好在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件

使用自定义事件优化Todo-List:

src/mian.js:

复制代码
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
    el:"#app",
    render: h => h(App),
    beforeCreate() {
        Vue.prototype.$bus = this
    }
})

src/components/App.vue

复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
            <MyHeader @addTodo="addTodo"/>
            <MyList :todos="todos"/>
            <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
            </div>
        </div>
    </div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import MyList from './components/MyList.vue'
    import MyFooter from './components/MyFooter.vue'

    export default {
        name:'App',
        components: { MyHeader,MyList,MyFooter },
        data() {
            return {
                todos:JSON.parse(localStorage.getItem('todos')) || []
            }
        },
        methods:{
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkTodo(id){
                this.todos.forEach((todo)=>{
                    if(todo.id === id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(id){
                this.todos = this.todos.filter(todo => todo.id !== id)
            },
            //全选or取消勾选
            checkAllTodo(done){
                this.todos.forEach(todo => todo.done = done)
            },
            //删除已完成的todo
            clearAllTodo(){
                this.todos = this.todos.filter(todo => !todo.done)
            }
        },
        watch:{
            todos:{
                deep:true,
                handler(value){
                    localStorage.setItem('todos',JSON.stringify(value))
                }
            }
        },
        mounted(){
            this.$bus.$on('checkTodo',this.checkTodo)
            this.$bus.$on('deleteTodo',this.deleteTodo)
        },
        beforeDestroy(){
            this.$bus.$off(['checkTodo','deleteTodo'])
        }
    }
</script>

<style>
    body {
        background: #fff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        font-size: 14px;
        line-height: 20px;
        text-align: center;
        vertical-align: middle;
        cursor: pointer;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #fff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #fff;
        background-color: #bd362f;
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }
    .todo-container .todo-wrap {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

src/components/MyItem.vue:

复制代码
<template>
    <li>
        <label>
            <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>
            <span>{{todo.title}}</span>
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id,todo.title)">删除</button>
    </li>
</template>

<script>
    export default {
        name:'MyItem',
        props:['todo'],
        methods:{
            handleCheck(id){
                this.$bus.$emit('checkTodo',id)
            },
            handleDelete(id,title){
                if(confirm("确定删除任务:"+title+"吗?")){
                    this.$bus.$emit('deleteTodo',id)
                }
            }
        }
    }
</script>

<style scoped>
    li {
        list-style: none;
        height: 36px;
        line-height: 36px;
        padding: 0 5px;
        border-bottom: 1px solid #ddd;
    }

    li label {
        float: left;
        cursor: pointer;
    }

    li label li input {
        vertical-align: middle;
        margin-right: 6px;
        position: relative;
        top: -1px;
    }

    li button {
        float: right;
        display: none;
        margin-top: 3px;
    }

    li:before {
        content: initial;
    }

    li:last-child {
        border-bottom: none;
    }

    li:hover {
        background-color: #eee;
    }

    li:hover button{
        display: block;
    }
</style>

3.11. 消息的订阅与发布

src/components/School.vue:

复制代码
<template>
	<div class="school">
		<h2>学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
	</div>
</template>

<script>
	import pubsub from 'pubsub-js'

	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷',
				address:'北京',
			}
		},
		methods:{
			demo(msgName,data) {
				console.log('我是School组件,收到了数据:',data)
			}
		},
		mounted() {
			this.pubId = pubsub.subscribe('demo',this.demo) //订阅消息
		},
		beforeDestroy() {
			pubsub.unsubscribe(this.pubId) //取消订阅
		}
	}
</script>

<style scoped>
	.school{
		background-color: skyblue;
		padding: 5px;
	}
</style>

src/components/Student.vue:

复制代码
<template>
	<div class="student">
		<h2>学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
		<button @click="sendStudentName">把学生名给School组件</button>
	</div>
</template>

<script>
	import pubsub from 'pubsub-js'

	export default {
		name:'Student',
		data() {
			return {
				name:'JOJO',
				sex:'男',
			}
		},
		methods: {
			sendStudentName(){
				pubsub.publish('demo',this.name) //发布消息
			}
		}
	}
</script>

<style scoped>
	.student{
		background-color: pink;
		padding: 5px;
		margin-top: 30px;
	}
</style>

总结:

消息订阅与发布(pubsub):

  1. 消息订阅与发布是一种组件间通信的方式,适用于任意组件间通信

  2. 使用步骤:

1.安装pubsub:npm i pubsub-js

2.引入:import pubsub from 'pubsub-js'

3.接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身

复制代码
export default {
    methods(){
        demo(data){...}
    }
    ...
    mounted() {
		this.pid = pubsub.subscribe('xxx',this.demo)
    }
}

4.提供数据:pubsub.publish('xxx',data)

5.最好在beforeDestroy钩子中,使用pubsub.unsubscribe(pid)取消订阅

使用消息的订阅与发布优化Todo-List:

src/App.vue:

复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
            <MyHeader @addTodo="addTodo"/>
            <MyList :todos="todos"/>
            <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
            </div>
        </div>
    </div>
</template>

<script>
    import pubsub from 'pubsub-js'
    import MyHeader from './components/MyHeader.vue'
    import MyList from './components/MyList.vue'
    import MyFooter from './components/MyFooter.vue'


    export default {
        name:'App',
        components: { MyHeader,MyList,MyFooter },
        data() {
            return {
                todos:JSON.parse(localStorage.getItem('todos')) || []
            }
        },
        methods:{
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkTodo(_,id){
                this.todos.forEach((todo)=>{
                    if(todo.id === id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(id){
                this.todos = this.todos.filter(todo => todo.id !== id)
            },
            //全选or取消勾选
            checkAllTodo(done){
                this.todos.forEach(todo => todo.done = done)
            },
            //删除已完成的todo
            clearAllTodo(){
                this.todos = this.todos.filter(todo => !todo.done)
            }
        },
        watch:{
            todos:{
                deep:true,
                handler(value){
                    localStorage.setItem('todos',JSON.stringify(value))
                }
            }
        },
        mounted(){
            this.pubId = pubsub.subscribe('checkTodo',this.checkTodo)
            this.$bus.$on('deleteTodo',this.deleteTodo)
        },
        beforeDestroy(){
            pubsub.unsubscribe(this.pubId)
            this.$bus.$off('deleteTodo')
        }
    }
</script>

<style>
    body {
        background: #fff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        font-size: 14px;
        line-height: 20px;
        text-align: center;
        vertical-align: middle;
        cursor: pointer;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #fff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #fff;
        background-color: #bd362f;
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }
    .todo-container .todo-wrap {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

src/components/myItem.vue:

html 复制代码
<template>
    <li>
        <label>
            <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>
            <span>{{todo.title}}</span>
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id,todo.title)">删除</button>
    </li>
</template>

<script>
    import pubsub from 'pubsub-js'
    export default {
        name:'MyItem',
        props:['todo'],
        methods:{
            handleCheck(id){                    
                pubsub.publish('checkTodo',id)
            },
            handleDelete(id,title){
                if(confirm("确定删除任务:"+title+"吗?")){
                    this.$bus.$emit('deleteTodo',id)
                }
            }
        }
    }
</script>

<style scoped>
    li {
        list-style: none;
        height: 36px;
        line-height: 36px;
        padding: 0 5px;
        border-bottom: 1px solid #ddd;
    }

    li label {
        float: left;
        cursor: pointer;
    }

    li label li input {
        vertical-align: middle;
        margin-right: 6px;
        position: relative;
        top: -1px;
    }

    li button {
        float: right;
        display: none;
        margin-top: 3px;
    }

    li:before {
        content: initial;
    }

    li:last-child {
        border-bottom: none;
    }

    li:hover {
        background-color: #eee;
    }

    li:hover button{
        display: block;
    }
</style>

3.12. $nextTick

使用$nextTick优化Todo-List:

src/App.vue:

html 复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
            <MyHeader @addTodo="addTodo"/>
            <MyList :todos="todos"/>
            <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
            </div>
        </div>
    </div>
</template>

<script>
    import pubsub from 'pubsub-js'
    import MyHeader from './components/MyHeader.vue'
    import MyList from './components/MyList.vue'
    import MyFooter from './components/MyFooter.vue'


    export default {
        name:'App',
        components: { MyHeader,MyList,MyFooter },
        data() {
            return {
                todos:JSON.parse(localStorage.getItem('todos')) || []
            }
        },
        methods:{
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkTodo(_,id){
                this.todos.forEach((todo)=>{
                    if(todo.id === id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(id){
                this.todos = this.todos.filter(todo => todo.id !== id)
            },
            //更新一个todo
			updateTodo(id,title){
				this.todos.forEach((todo)=>{
					if(todo.id === id) todo.title = title
				})
			},
            //全选or取消勾选
            checkAllTodo(done){
                this.todos.forEach(todo => todo.done = done)
            },
            //删除已完成的todo
            clearAllTodo(){
                this.todos = this.todos.filter(todo => !todo.done)
            }
        },
        watch:{
            todos:{
                deep:true,
                handler(value){
                    localStorage.setItem('todos',JSON.stringify(value))
                }
            }
        },
        mounted(){
            this.pubId = pubsub.subscribe('checkTodo',this.checkTodo)
            this.$bus.$on('deleteTodo',this.deleteTodo)
            this.$bus.$on('updateTodo',this.updateTodo)
        },
        beforeDestroy(){
            pubsub.unsubscribe(this.pubId)
            this.$bus.$off('deleteTodo')
            this.$bus.$off('updateTodo')
        }
    }
</script>

<style>
    body {
        background: #fff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        font-size: 14px;
        line-height: 20px;
        text-align: center;
        vertical-align: middle;
        cursor: pointer;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #fff;
        background-color: #e04e49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #fff;
        background-color: #bd362f;
    }

    .btn-info {
        color: #fff;
        background-color: rgb(50, 129, 233);
        border: 1px solid rgb(1, 47, 212);
        margin-right: 5px;
    }

    .btn-info:hover {
        color: #fff;
        background-color: rgb(1, 47, 212);
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }
    .todo-container .todo-wrap {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

src/components/MyItem.vue:

html 复制代码
<template>
    <li>
        <label>
            <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>
            <span v-show="!todo.isEdit">{{todo.title}}</span>
            <input type="text" v-show="todo.isEdit" :value="todo.title" @blur="handleBlur(todo,$event)" ref="inputTitle">
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id,todo.title)">删除</button>
        <button class="btn btn-info" v-show="!todo.isEdit" @click="handleEdit(todo)">编辑</button>
    </li>
</template>

<script>
    import pubsub from 'pubsub-js'
    export default {
        name:'MyItem',
        props:['todo'],
        methods:{
            handleCheck(id){                    
                pubsub.publish('checkTodo',id)
            },
            handleDelete(id,title){
                if(confirm("确定删除任务:"+title+"吗?")){
                    this.$bus.$emit('deleteTodo',id)
                }
            },
            handleEdit(todo){
                // 如果todo自身有isEdit属性就将isEdit改成true
				if(Object.prototype.hasOwnProperty.call(todo,'isEdit')){
					todo.isEdit = true
				}else{
                    // 如果没有就向todo中添加一个响应式的isEdit属性并设为true
					this.$set(todo,'isEdit',true)
				}
                // 当Vue重新编译模板之后执行$nextTick()中的回调函数
                this.$nextTick(function(){
                    // 使input框获取焦点
                    this.$refs.inputTitle.focus()
                })
			},
            // 当input框失去焦点时更新
            handleBlur(todo,event){
                todo.isEdit = false
				if(!event.target.value.trim()) return alert('输入不能为空!')
				this.$bus.$emit('updateTodo',todo.id,event.target.value)
            }
        }
    }
</script>

<style scoped>
    li {
        list-style: none;
        height: 36px;
        line-height: 36px;
        padding: 0 5px;
        border-bottom: 1px solid #ddd;
    }

    li label {
        float: left;
        cursor: pointer;
    }

    li label li input {
        vertical-align: middle;
        margin-right: 6px;
        position: relative;
        top: -1px;
    }

    li button {
        float: right;
        display: none;
        margin-top: 3px;
    }

    li:before {
        content: initial;
    }

    li:last-child {
        border-bottom: none;
    }

    li:hover {
        background-color: #eee;
    }

    li:hover button{
        display: block;
    }
</style>

Todo-List最终效果:

总结:

$nextTick:

相关推荐
Deng9452013141 小时前
基于Python的职位画像系统设计与实现
开发语言·python·文本分析·自然语言处理nlp·scrapy框架·gensim应用
一只小青团4 小时前
Python之面向对象和类
java·开发语言
qq_529835354 小时前
ThreadLocal内存泄漏 强引用vs弱引用
java·开发语言·jvm
景彡先生5 小时前
C++并行计算:OpenMP与MPI全解析
开发语言·c++
LuciferHuang5 小时前
震惊!三万star开源项目竟有致命Bug?
前端·javascript·debug
GISer_Jing5 小时前
前端实习总结——案例与大纲
前端·javascript
天天进步20156 小时前
前端工程化:Webpack从入门到精通
前端·webpack·node.js
量子联盟6 小时前
原创-基于 PHP 和 MySQL 的证书管理系统,免费开源
开发语言·mysql·php
姑苏洛言6 小时前
编写产品需求文档:黄历日历小程序
前端·javascript·后端
知识分享小能手7 小时前
Vue3 学习教程,从入门到精通,使用 VSCode 开发 Vue3 的详细指南(3)
前端·javascript·vue.js·学习·前端框架·vue·vue3