组件化编码流程 案例演示
-
实现静态组件: 抽取组件,使用组件实现静态页面效果
- 把js剔除,先保证页面正常的显示效果(比如css正常)
-
展示动态数据
- 数据的类型,名称是什么
- 数据保存在哪个组件
-
交互: 从绑定事件监听开始
-
"todo.png"页面组件化结构分析: 三个大组件,再嵌套子组件
- input框 可以命名为'MyHeader'组件(若命名为'Header',就和html <header>标签冲突)
- 备忘录一系列动作,可以命名为'MyList'组件
- 每一个列表项,可以命名为'MyItem'组件
- 已/未完成,可以命名为'MyFooter'组件
- 划分基本的组件结构
- components目录下,新建四个组件
- MyHeader.vue
- MyList.vue
- MyItem.vue
- MyFooter.vue
- 每块的内容如下
<template>
</template>
<script>
export default {
name:'MyHeader' // 其他组件名称自己填
}
</script>
<style>
</style>
- 在 App.vue中注册
<template>
<div id="root">
</div>
</template>
<script>
import MyHeader from'./components/FirstDemo/MyHeader.vue'
import MyList from'./components/FirstDemo/MyList.vue'
import MyFooter from'./components/FirstDemo/MyFooter.vue'
export default {
name: 'App',
components: { // 注册
MyHeader,
MyList,
MyFooter
},
}
</script>
- 把整体的html结构拆分后,各个组件代码如下
- App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader/> <!--应用三个组件-->
<MyList/>
<MyFooter/>
</div>
</div>
</div>
</template>
<script>
import MyHeader from'./components/FirstDemo/MyHeader.vue'
import MyList from'./components/FirstDemo/MyList.vue'
import MyFooter from'./components/FirstDemo/MyFooter.vue'
export default {
name: 'App',
components: {
MyHeader,
MyList,
MyFooter
},
}
</script>
<style>
/*base*/
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>
- MyHeader.vue
<template>
<div class="todo-header">
<!--输入框-->
<input type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
</template>
<script>
export default {
name:'MyHeader'
}
</script>
<style>
/*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>
- MyList.vue
<template>
<!--列表-->
<ul class="todo-main">
<MyItem/>
</ul>
</template>
<script>
import MyItem from './MyItem.vue'
export default {
name:'MyList',
components:{MyItem}
}
</script>
<style>
/*list*/
.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>
- MyItem.vue
<template>
<li> <!--列表项-->
<label>
<input type="checkbox"/>
<span>打篮球</span>
</label>
<button class="btn btn-danger">删除</button>
</li>
</template>
<script>
export default {
name:'MyItem'
}
</script>
<style>
/*item*/
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;
}
</style>
- MyFooter.vue
<template>
<div class="todo-footer">
<label>
<input type="checkbox" />
</label>
<span>
<span>已完成1</span><span>/全部3</span>
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name:'MyFooter'
}
</script>
<style>
/*footer*/
.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>
展示动态数据
- 先把list列表的数据,放在MyList组件
### MyList.vue
<template>
<ul class="todo-main">
<!--渲染子项数据,并把子项的数据传递给子组件-->
<MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" />
</ul>
</template>
<script>
import MyItem from './MyItem.vue'
export default {
name:'MyList',
data(){
return {
todos:[ // 准备数据
{id:'001',title:'打篮球',done:true},
{id:'002',title:'跑步',done:false},
{id:'003',title:'健身',done:true},
]
}
},
components:{MyItem}
}
</script>
<style>
......
</style>
### MyItem.vue
<template>
<li>
<label>
<!--渲染数据-->
<input type="checkbox" :checked="todo.done"/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" style="display: none;">删除</button>
</li>
</template>
<script>
export default {
name:'MyItem',
props:['todo'] // 接收MyList组件传过来的子项数据
}
</script>
<style>
......
</style>
- 至此,列表数据的渲染,暂告一段落
- 表单收集用户数据的两种方法
- v-model
<input type="text" v-model="title"/>
......
<script>
export default {
name:'MyHeader',
data(){
return {
title:'' // 收集用户数据
}
},
}
</script>
- 键盘事件: @keyup.enter="add" ,再自定义add方法获取用户输入的数据
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
......
<script>
export default {
name:'MyHeader',
methods:{
add(e){
console.log(e.target.value) // 获取用户输入的数据
}
}
}
</script>
-
将用户的输入包装成一个对象
- 先安装"uuid轻量版": npm i nanoid
<script>
import {nanoid} from 'nanoid'
export default {
name:'MyHeader',
......
methods:{
add(e){
// 包装对象
const todoObj = {id:nanoid(),title:e.target.value,done:false}
}
}
}
</script>
-
把包装好的对象,发给 MyList组件
- 要实现兄弟组件之间的通讯,以目前掌握的知识,还做不到(兄弟组件传值,,目前不可以)
- 所以我们把数据放到 App.vue来处理,让App传给MyList组件(父子组件传值,可以)
### MyList.vue
<template>
<ul class="todo-main">
<MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" />
</ul>
</template>
<script>
import MyItem from './MyItem.vue'
export default {
name:'MyList',
data(){
return {
// 注释掉
// todos:[
// {id:'001',title:'打篮球',done:true},
// {id:'002',title:'跑步',done:false},
// {id:'003',title:'健身',done:true},
// ]
}
},
components:{MyItem},
props:['todos'] // 接收 App.vue传过来的值(遍历完后传给子组件)
}
</script>
### APP.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader />
<MyList :todos="todos" /> <!--传值-->
<MyFooter/>
</div>
</div>
</div>
</template>
<script>
// FirstDemo
import MyHeader from'./components/FirstDemo/MyHeader.vue'
import MyList from'./components/FirstDemo/MyList.vue'
import MyFooter from'./components/FirstDemo/MyFooter.vue'
export default {
name: 'App',
data(){
return {
todos:[ // 数据放在这里
{id:'001',title:'打篮球',done:true},
{id:'002',title:'跑步',done:false},
{id:'003',title:'健身',done:true},
]
}
},
components: {
MyHeader,
MyList,
MyFooter
},
methods:{
......
}
}
</script>
- 在 App.vue中,传一个函数(可以携带参数)给 MyHeader 组件
MyHeader组件收集完用户的数据,打包成一个obj,当成参数回传给该函数即可
### App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :receive="receive" /> <!--传一个函数对象过去-->
<MyList :todos="todos" />
<MyFooter/>
</div>
</div>
</div>
</template>
<script>
// FirstDemo
import MyHeader from'./components/FirstDemo/MyHeader.vue'
import MyList from'./components/FirstDemo/MyList.vue'
import MyFooter from'./components/FirstDemo/MyFooter.vue'
export default {
name: 'App',
data(){
......
},
components: {
MyHeader,
MyList,
MyFooter
},
methods:{
receive(x){ // 用参数x来接收'包装对象'
// console.log('我是App组件,我收到了',x)
this.todos.unshift(x) // 把收到的包装对象,加入列表
}
}
}
</script>
### MyHeader.vue
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
</div>
</template>
<script>
import {nanoid} from 'nanoid'
export default {
name:'MyHeader',
data(){
return {
title:''
}
},
props:['receive'], // 接收
methods:{
add(e){
const todoObj = {id:nanoid(),title:e.target.value,done:false}
this.receive(todoObj); // 传值
e.target.value = ''; // 清空输入框
}
}
}
</script>
<style>
......
</style>
- 现在,可以测试效果了
- 把 MyHeader 组件的输入逻辑小改一下(换成v-model,避免操作DOM[e.target.value就是操作DOM])
### MyHeader.vue
<template>
<div class="todo-header">
<!-- <input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/> -->
<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
</div>
</template>
<script>
import {nanoid} from 'nanoid'
export default {
name:'MyHeader',
data(){
return {
title:'' // 占坑
}
},
props:['addTodo'],
methods:{
add(e){
if(!this.title.trim()) return alert('输入不能为空');
const todoObj = {id:nanoid(),title:e.target.value,done:false}
this.addTodo(todoObj);
// e.target.value = '';
this.title = ''; // 修改成这样
}
}
}
</script>
<style>
......
</style>
组件化编程重要思想
- 数据存放在哪一个组件,该数据对应的增,删,改,查方法就写在该组件
勾选框处理
-
思路: 获取该列表项的id,通知App.vue修改 done属性值,取反即可
- App组件(爷爷)=>MyList组件(父亲)=>MyItem组件(孙子)
### App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo" />
<!--传函数对象给MyList组件-->
<MyList :todos="todos" :checkTodo="checkTodo" />
<MyFooter/>
</div>
</div>
</div>
</template>
<script>
// FirstDemo
import MyHeader from'./components/FirstDemo/MyHeader.vue'
import MyList from'./components/FirstDemo/MyList.vue'
import MyFooter from'./components/FirstDemo/MyFooter.vue'
export default {
name: 'App',
data(){
return {
todos:[
{id:'001',title:'打篮球',done:true},
{id:'002',title:'跑步',done:false},
{id:'003',title:'健身',done:true},
]
}
},
components: {
......
},
methods:{
addTodo(x){
......
},
// 定义好函数以后,给孙组件调用
checkTodo(id){
this.todos.forEach((todo)=>{
if(todo.id==id) todo.done=!todo.done // 取反
})
}
}
}
</script>
### MyList.vue
<template>
<ul class="todo-main">
<MyItem v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
:checkTodo="checkTodo" /> <!--传给子组件-->
</ul>
</template>
<script>
import MyItem from './MyItem.vue'
export default {
name:'MyList',
data(){
......
},
components:{MyItem},
props:['todos','checkTodo'] // 被动接收 checkTodo
}
</script>
<style>
......
</style>
### MyItem.vue
<template>
<li>
<label>
<!--点击事件处理勾选的逻辑-->
<!--这里使用@change一模一样效果-->
<input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" style="display: none;">删除</button>
</li>
</template>
<script>
export default {
name:'MyItem',
props:['todo','checkTodo'], // 接收父组件传过来的函数对象...
methods:{
handleCheck(id){
// console.log(id)
this.checkTodo(id) // 调用爷组件函数
}
}
}
</script>
- 测试勾选效果成功~
- 注意事项:这里还有一个简单粗暴的实现方式,但不推荐这么做
因为会修改props配置项传过来的值(vue不推荐,vue只希望读,而不去修改)
- 这种处理方式修改了props值,但是vue不会警告(vue只浅层检测对象整体变化才会警告)
对于对象的部分属性修改,vue监测不到
<template>
<li>
<label>
<!-- <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/> -->
<!-- <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/> -->
<!--利用v-model双向绑定,简单粗暴实现-->
<input type="checkbox" :checked="todo.done" v-model="todo.done" />
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" style="display: none;">删除</button>
</li>
</template>
<script>
export default {
name:'MyItem',
props:['todo','checkTodo'],
methods:{
handleCheck(id){
// console.log(id)
this.checkTodo(id)
}
}
}
</script>
删除列表项(逻辑和勾选类似)
- 思路: 利用filter()过滤掉被删除的id,展示其他列表项即可
### app.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo" /> <!--传给MyList-->
<MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo" />
<MyFooter/>
</div>
</div>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
import School from './components/School.vue'
import Student from './components/Student.vue'
// FirstDemo
import MyHeader from'./components/FirstDemo/MyHeader.vue'
import MyList from'./components/FirstDemo/MyList.vue'
import MyFooter from'./components/FirstDemo/MyFooter.vue'
export default {
name: 'App',
data(){
return {
todos:[
{id:'001',title:'打篮球',done:true},
{id:'002',title:'跑步',done:false},
{id:'003',title:'健身',done:true},
]
}
},
components: {
MyHeader,
MyList,
MyFooter
},
methods:{
addTodo(x){
......
},
checkTodo(id){
......
},
deleteTodo(id){
// 利用过滤
this.todos = this.todos.filter(todo=>todo.id!==id)
}
}
}
</script>
### MyList(被动接收)
<template>
<ul class="todo-main">
<MyItem v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
:checkTodo="checkTodo"
:deleteTodo="deleteTodo" /> <!--传值-->
</ul>
</template>
<script>
import MyItem from './MyItem.vue'
export default {
name:'MyList',
data(){
return {
......
}
},
components:{MyItem},
props:['todos','checkTodo','deleteTodo'] // 被动接收
}
</script>
### MyItem
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
<span>{{todo.title}}</span>
</label> <!--删除的逻辑-->
<button class="btn btn-danger" @click="handDelete(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name:'MyItem',
props:['todo','checkTodo','deleteTodo'],
methods:{
handleCheck(id){
this.checkTodo(id)
},
handDelete(id){
if(confirm('确定删除吗?')){
// console.log(id)
this.deleteTodo(id) // 调用爷组件的逻辑
}
}
}
}
</script>
- 测试效果成功~
MyFooter组件功能实现
- 实现 已完成xxx/全部yyy
- 思路: App组件把todos传给MyFooter组件
- 全部: todos.length
- 已完成: 定义一个变量i,遍历每一项,只有todo.done=true,i自加
### App.vue
......
<div class="todo-container">
<div class="todo-wrap">
<MyHeader ... />
<MyList ... />
<MyFooter :todos="todos" /> <!--传值-->
</div>
</div>
### MyFooter.vue
......
<template>
<div class="todo-footer">
<label>
......
</label>
<span><!--用计算属性返回最终的结果--> <!--全部数量-->
<span>已完成{{doneTotal}}</span><span>/全部{{todos.length}}</span>
</span>
......
</div>
</template>
<script>
export default {
name:'MyFooter',
props:['todos'],
computed:{
doneTotal(){
let i = 0;
this.todos.forEach((todo)=>{
if(todo.done == true)i++
})
return i;
}
}
}
</script>
-
计算'已完成'时,我们可以换一种'高大上'的写法
- arr.reduce: 数组条件统计
### MyFooter.vue
......
<script>
export default {
name:'MyFooter',
props:['todos'],
computed:{
doneTotal(){
// let i = 0;
// this.todos.forEach((todo)=>{
// if(todo.done == true)i++
// })
// return i;
return this.todos.reduce((pre,obj)=>{return pre + (obj.done? 1 : 0)},0)
}
}
}
</script>
-
实现'全选/全不选'
- 当用户勾选所有的列表子项时,自动勾选'全选'
<template>
<div class="todo-footer">
<label> <!--使用计算属性-->
<input type="checkbox" :checked="isAll"/>
</label>
<span> <!--这里改造成'计算属性'-->
<span>已完成{{doneTotal}}</span><span>/全部{{total}}</span>
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name:'MyFooter',
props:['todos'],
computed:{
total(){
return this.todos.length
},
doneTotal(){
return this.todos.reduce((pre,obj)=>{return pre + (obj.done? 1 : 0)},0)
},
isAll(){ // 如果子项数量和子项总数相等,那么就是true,从而实现'全选'
return this.total === this.doneTotal && this.total > 0
}
}
}
</script>
全选/全不选 功能实现
-
场景分析
-
当用户勾选子项的时候,若全部都勾选了,那么 checkbox 就全选
-
当点击 checkbox 时,实现子项的'全选'/'全不选'
-
### MyFooter.vue
<template>
<div class="todo-footer">
<label> <!--关注之处-->
<input type="checkbox" :checked="isAll" @change="checkAll" />
</label>
<span>
<span>已完成{{doneTotal}}</span><span>/全部{{total}}</span>
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name:'MyFooter',
props:['todos','checkAllTodo'], // 接收
computed:{
total(){
return this.todos.length
},
doneTotal(){
return this.todos.reduce((pre,obj)=>{return pre + (obj.done? 1 : 0)},0)
},
isAll(){ // checkbox什么自动勾?满足如下条件就勾
return this.total === this.doneTotal && this.total > 0
}
},
methods:{
checkAll(e){
// console.log(e.target.checked)
// 获取checkbox的状态值,传给爷组件处理
this.checkAllTodo(e.target.checked)
},
}
}
</script>
### App.vue
......
<div class="todo-container">
<div class="todo-wrap">
<MyHeader ...... />
<MyList ...... />
<!--传函数给孙组件调用-->
<MyFooter :todos="todos" :checkAllTodo="checkAllTodo"/>
</div>
</div>
......
methods:{
addTodo(x){
......
},
checkTodo(id){
......
},
deleteTodo(id){
......
},
checkAllTodo(done){
this.todos.forEach((todo)=>{
todo.done = done // 遍历修改done值
})
}
}
- 另一种实现 checkbox 全选/全不选 的方式,使用 v-model
<template>
<div class="todo-footer">
<label>
<!-- <input type="checkbox" :checked="isAll" @change="checkAll" /> -->
<!--使用v-model实现读取/输出-->
<input type="checkbox" v-model="isAll" />
</label>
......
</div>
</template>
<script>
export default {
name:'MyFooter',
props:['todos','checkAllTodo'],
computed:{
total(){
......
},
doneTotal(){
......
},
// isAll(){
// return this.total === this.doneTotal && this.total > 0
// // return this.total === this.doneTotal
// }
isAll:{
get(){
// 把之前逻辑搞过来即可
return this.total === this.doneTotal && this.total > 0
},
set(value){ // value就是用户选中/未选中的值
return this.checkAllTodo(value)
}
}
},
// methods:{
// checkAll(e){
// console.log(e.target.checked)
// this.checkAllTodo(e.target.checked)
// },
// }
}
</script>
清除已完成任务
- 思路分析: 爷组件把obj.done为true的子项过滤出来,取反即可
孙组件使用按钮点击事件,调用这段逻辑即可
### App.vue
<div class="todo-container">
<div class="todo-wrap">
<MyHeader ...... />
<MyList ...... />
<!--传参-->
<MyFooter ...... :clearTodoDone="clearTodoDone" />
</div>
</div>
......
methods:{
......
clearTodoDone(){
this.todos = this.todos.filter((todo)=>{
return !todo.done // 过滤取反
})
}
}
### MyFooter.vue
......
<template>
<div class="todo-footer">
...... <!--绑定事件-->
<button class="btn btn-danger" @click="clearDone">清除已完成任务</button>
</div>
</template>
......
<script>
export default {
name:'MyFooter',
props:['todos','checkAllTodo','clearTodoDone'], // 接收
computed:{
total(){
......
},
doneTotal(){
......
},
isAll:{
......
}
},
methods:{
checkAll(e){
......
},
clearDone(){
this.clearTodoDone(); // 调用
}
}
}
</script>
总结
-
组件化编码流程
-
拆分静态组件: 组件要按照功能点拆分,命名不要与html元素冲突
-
实现动态组件: 考虑好数据存放的位置,数据是一个组件在用,还是多个组件在用:
- 一个组件在用: 放在组件自身即可
- 多个组件在用: 放在他们共同的父组件上(状态提升)
-
实现交互: 从绑定事件开始
-
-
props适用于:
- 父组件 ==> 子组件通信
- 子组件 ==> 父组件通信(要求父先给子一个函数)
-
使用 "v-model" 要切记: v-model 绑定的值不能是 props传过来的值(vue建议不可修改)
-
props传过来的若是对象类型的值,修改对象中的属性vue不会报错,但不推荐这样做
浏览器本地存储
-
保存数据到浏览器
-
读取浏览器存储的数据
-
删除浏览器存储的数据
-
demo如下
...... <body> <h2>浏览器存储Demo</h2> <button type="button" onclick="saveData()">保存数据</button> <button type="button" onclick="readData()">读取数据</button> <button type="button" onclick="delData()">删除数据</button> <script type="text/javascript"> function saveData(){ // 可以省略windows对象 localStorage.setItem('name','JimGreen') }; function readData(){ console.log(localStorage.getItem('name')) }; function delData(){ localStorage.removeItem('name') } </script> </body>
-
注意数据类型
- 比如 Number会被转成 String
- 存储对象类型的数据时,注意序列化
......
<script type="text/javascript">
var obj = {name:'Kate Green',age:18}
function saveData(){
localStorage.setItem('name','JimGreen');
localStorage.setItem('age',666); // 666被转换成字符串
localStorage.setItem('obj',JSON.stringify(obj)); // 序列化对象
};
function readData(){
......
};
function delData(){
......
}
</script>
-
同样的,读取数据的时候,也要注意将对象字符串转换成js对象
...... <script type="text/javascript"> var obj = {name:'Kate Green',age:18} function saveData(){ ...... localStorage.setItem('obj',JSON.stringify(obj)); }; function readData(){ console.log(localStorage.getItem('name')) console.log(localStorage.getItem('age')) console.log(localStorage.getItem('obj')) // 字符串对象 var personObj = localStorage.getItem('obj') console.log(JSON.parse(personObj)) // js对象 }; function delData(){ ...... } </script>
-
清空所有数据
...... function clearAllData(){ localStorage.clear() }
sessionStorage 的API是一模一样的,不再演示(浏览器关闭,会话就消失)
WebStorage 小结
-
存储内容大小一般支持5MB左右(不同浏览器可能还不一样)
-
浏览器端通过"Windows.sessionStorage"和"Windows.localStorage"属性来实现本地存储机制
-
相关API
- xxx.setItem(key,value); - xxx.getItem(key); - xxx.removeItem(key); - xxx.clear()
-
备注:
- SessionStorage存储的内容会随着浏览器窗口关闭而消失
- LocalStorage存储的内容,需要手动清除才会消失
- xxx.getItem(yyy),如果yyy对应的value获取不到,该API会返回 null
- JSON.parse(null)的结果依然是null
自定义事件
-
作用: 实现组件之间的数据传输(子传父)
- 之前的实现方式: 父组件传给子组件一个函数类型的props
-
demo演示
### app.vue <template> <div id="root"> <h1>{{msg}}</h1> <!--之前的写法--> <MySchool :getSchoolName="getSchoolName"></MySchool> <!--绑定peiqi事件到demo回调函数--> <MyStudent v-on:peiqi="demo"></MyStudent> </div> </template> <script> // DefineEvent import MySchool from './components/DefineEvent/MySchool.vue' import MyStudent from './components/DefineEvent/MyStudent.vue' export default { name: 'App', data(){ return { msg:'Welcome!' } }, components: { MySchool, // 注册两个组件 MyStudent }, methods:{ getSchoolName(name){ console.log('app收到了: ',name) // 之前的写法 }, demo(name){ // 接收子组件传过来的name参数 console.log('app的demo方法被调用了',name) } } } </script> <style> ...... </style> ### MyStudent.vue <template> <div class="demo"> <h3>名字:{{studentName}}</h3> <h3>性别:{{studentSex}}</h3> <!--绑定点击事件--> <button type="button" @click="sendStudentName">发送学生名字</button> </div> </template> <script> export default { name:'Student', data(){ return { studentName:'Jim Green', studentSex:'male' } }, methods:{ sendStudentName(){ // 通过'$emit(自定义事件名,*参数)'传参 this.$emit('peiqi',this.studentName) } } } </script> <style scoped> ...... </style>
-
自定义事件小结
- 子组件通过'$emit(自定义事件名,*参数)' 自定义事件名并传参 - 父组件在子组件标签中,通过v-on接收子组件的'自定义事件名称',通过自定义demo函数(回调函数)来接收其它参数,并处理逻辑 <MyStudent v-on:peiqi="demo"></MyStudent>
-
自定义事件的另一种写法,通过定义'ref'属性实现
### App.vue <template> <div id="root"> <h1>{{msg}}</h1> <MySchool :getSchoolName="getSchoolName"></MySchool> <!-- <MyStudent v-on:peiqi="demo"></MyStudent> --> <!--使用ref属性标识MyStudent组件--> <MyStudent ref="student"></MyStudent> </div> </template> <script> // DefineEvent import MySchool from './components/DefineEvent/MySchool.vue' import MyStudent from './components/DefineEvent/MyStudent.vue' export default { name: 'App', data(){ ...... }, components: { MySchool, MyStudent }, methods:{ getSchoolName(name){ ...... }, getStudentName(name){ console.log('app的getStudentName方法被调用了',name) } }, mounted(){ // 组件挂载完立即执行 // 接收子组件绑定的自定义事件 $on('子组件自定义事件',回调函数) this.$refs.student.$on('peiqi',this.getStudentName) } } </script> <style> ...... </style>
-
注意事项:若想让自定义事件只执行一次,可以这么写(使用'$once')
### app.vue ...... mounted(){ // 变更之处 this.$refs.student.$once('peiqi',this.getStudentName) }
### app.vue <template> <div id="root"> <h1>{{msg}}</h1> <MySchool :getSchoolName="getSchoolName"></MySchool> <!--之前的写法,用".once"也可以--> <MyStudent @peiqi.once="getStudentName"></MyStudent> <!-- <MyStudent v-on:peiqi="getStudentName"></MyStudent> --> <!-- <MyStudent ref="student"></MyStudent> --> </div> </template>
-
涉及到子组件传递多个参数的时候,父组件的回调函数可以这么设计
### MySchool.vue ...... methods:{ sendSchoolName(){ // 传递多个参数 this.getSchoolName(this.schoolName,111,222,333) } } ### App.vue ...... methods:{ // 通过'...params'方式接收多个参数 getSchoolName(name,...params){ // params是一个list console.log('app收到了: ',name,params) }, ...... },
-
自定义事件'this坑'演示(app接收子组件传过来的数据并展示)
### App.vue <template> <div id="root"> <h1>{{msg}}--{{title}}</h1> <MySchool ......> <!--使用ref方式--> <MyStudent ref="student"></MyStudent> </div> </template> <script> // DefineEvent import MySchool from './components/DefineEvent/MySchool.vue' import MyStudent from './components/DefineEvent/MyStudent.vue' export default { name: 'App', data(){ ...... }, components: { MySchool, MyStudent }, methods:{ getSchoolName(name,...params){ ...... }, // StudentName(name){ // nsole.log('app的getStudentName方法被调用了',name) // is.title = name // } }, mounted(){ // 把回调函数写在里面,结果无效果,this指向子组件MyStudent this.$refs.student.$on('peiqi',function showName(name){ console.log(name); console.log(this); // this指向子组件 this.title=name; // 子组件没有title,所以赋值不会生效 }) } } </script> <style> #root { height: 400px; background: red; } </style>
-
this坑解决办法:使用 箭头函数 代替 普通函数 写法即可
### App.vue ...... mounted(){ // this.$refs.student.$once('peiqi',this.getStudentName) // this.$refs.student.$on('peiqi',function showName(name){ // console.log(name); // console.log(this); // this.title=name; // }) this.$refs.student.$on('peiqi',(name)=>{ console.log(name); console.log(this); // 指向app组件 this.title=name; // 赋值成功 }) }
-
给子组件绑定'@click事件',该事件会生效吗?答案是不会生效(vue不会当成原生的click事件,故不生效)
### App.vue <template> <div id="root"> ...... <!--绑定click事件--> <MyStudent ref="student" @click="show"></MyStudent> </div> </template> ...... methods:{ getSchoolName(name,...params){ ...... }, getStudentName(name){ ...... }, show(){ alert('show message ...') // 写点逻辑,点击子组件区域块,该事件不会被触发 } }
-
解决办法,加上'.native'即可
...... <div id="root"> ...... <!--增加之处--> <MyStudent ref="student" @click.native="show"></MyStudent> </div>
自定义事件总结
- 作用: 一种组件间通信的方式,适用于: 子组件 ===> 父组件
- 使用场景:
- 若子组件想给父组件传递数据,那么在父组件中给子组件绑定自定义事件(该事件的回调在父组件中)
- 两种绑定方式
- 在父组件中, <Demo @peiqi="test"> 或 <Demo v-on:peiqi="test">
- 在父组件中
<Demo ref="demo">
......
mounted(){
this.$refs.xxx.$on('peiqi',this.test)
}
- 若想让自定义事件只触发一次,可以使用'once'修饰符或者'$once'方法
- 触发自定义事件: this.$emit('peiqi',数据参数)
- 解绑自定义事件: this.$off('peiqi')
- 组件上也可以绑定原生DOM事件,需要使用'native'修饰符
- 注意事项: 通过"this.$refs.xxx.$on('peiqi',回调)"绑定自定义事件时,回调
- 要么配置在 methods 中
- 要么使用 箭头函数
否则 this 指向会出问题
把toDoList案例改造成自定义事件
### App.vue
<div class="todo-container">
<div class="todo-wrap">
<! 不再用这种方式接收参数>
<!-- <MyHeader :addTodo="addTodo" /> -->
<!--改造成这样,去子组件触发一下-->
<MyHeader @addTodo="addTodo" />
<MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo" />
<MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearTodoDone="clearTodoDone" />
</div>
</div>
### MyHeader.vue
<template>
<div class="todo-header">
......
</div>
</template>
<script>
import {nanoid} from 'nanoid'
export default {
name:'MyHeader',
data(){
return {
title:''
}
},
// props:['addTodo'], // 注释掉
methods:{
add(e){
if(!this.title.trim()) return alert('输入不能为空');
const todoObj = {id:nanoid(),title:e.target.value,done:false}
this.$emit('addTodo',todoObj) // 触发自定义事件并传参
// this.addTodo(todoObj); // 注释掉
this.title = '';
}
}
}
</script>
-
注意事项: 如果是传数据,比如list之类的,就不可以使用自定义事件去处理
...... <div class="todo-container"> <div class="todo-wrap"> ...... <!--这里就不能改,传递数据给子组件--> <!--这边两个都可以改--> <MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearTodoDone="clearTodoDone" /> </div> </div>
全局事件总线
-
必须满足两个条件
-
所有组件都能访问
-
有'$on','$emit'方法
-
-
准备场景(一个School组件和一个Student组件)
### School.vue <template> <div class="demo"> <h3>名称:{{name}}</h3> <h3>地址:{{address}}</h3> </div> </template> <script> export default { name:'School', data(){ return { name:'顶尖', address:'厦门', } }, mounted(){ console.log(Window.x) } } </script> <style scoped> .demo { background-color: #08C63E; } </style> ### Student.vue <template> <div class="demo"> <h3>名字:{{name}}</h3> <h3>年龄:{{age}}</h3> </div> </template> <script> export default { name:'Student', data(){ return { name:'Jim Green', age:20 } }, } </script> <style scoped> .demo { background-color: skyblue; } </style>
-
设计思路一,绑定到 Window 上面
- 所有组件都能访问,但是和事件相关的API无法调用(只有vm/vc实例才有这些API)
### main.js ...... Window.x = {a:1,b:2} // 给Window对象绑定一个属性x,并赋值 ### School.vue ...... <script> export default { name:'School', data(){ ...... }, mounted(){ console.log(Window.x) // 没毛病 } } </script>
-
解决方式:绑定到Vue原型对象上
- 一般取名为'$bus'(翻译为: 总线/公交车)
### main.js ...... Vue.prototype.$bus = {a:1,b:2} // 赋值 ### School.vue ...... <script> export default { ...... mounted(){ console.log(this.$bus) // 收到赋值 } } </script>
- 测试一下,绑定事件demo
### main.js ...... // Window.x = {a:1,b:2} new Vue({ render: h => h(App), beforeCreate(){ // 安装全局事件总线 Vue.prototype.$bus = this // 定义全局事件总线$bus,赋值vm实例 } }).$mount('#app') ### School.vue <template> ...... </template> <script> export default { name:'School', data(){ ...... }, mounted(){ // 绑定事件并接收数据参数 this.$bus.$on('sendStudentName',(name)=>{ console.log('School组件收到了数据',name) }) }, // 组件被销毁之前,解绑事件(释放事件资源) beforeDestroy(){ this.$bus.$off('sendStudentName') } } </script> ### Student.vue <template> <div class="demo"> <h3>名字:{{name}}</h3> <h3>年龄:{{age}}</h3> <!--点击事件--> <button type="button" @click="sendStudentName">发送学生名字</button> </div> </template> <script> export default { name:'Student', data(){ return { name:'Jim Green', age:20 } }, methods:{ sendStudentName(){ // 触发事件并传参 this.$bus.$emit('sendStudentName',this.name) } } } </script> <style scoped> .demo { background-color: skyblue; } </style>
全局事件总线(GlobalEventBus)
-
一种组件间通信的方式,适用于任意组件间通信
-
安装全局事件总线
### main.js ...... new Vue({ render: h => h(App), beforeCreate(){ Vue.prototype.$bus = this // 关注之处 } }).$mount('#app')
-
使用事件总线
-
接收数据: A组件想接收数据,则在A组件中给'$bus'绑定自定义事件,事件的回调函数留在A组件自身
绑定事件以后,最好写一个 beforeDestroy() 解绑事件,以便释放资源
...... data(){ ...... }, mounted(){ this.$bus.$on('sendStudentName',(name)=>{ console.log('School组件收到了数据',name) }) }, beforeDestroy(){ this.$bus.$off('sendStudentName') }
-
提供数据
this.$bus.$emit('sendStudentName',数据)
-
组件之间通讯方式 review
-
父传子==>props
-
子传父==>props/自定义事件/全局事件
-
改造之前的"toDoList"案例,使用全局事件总线来处理 爷组件==>孙组件 之间传参(跳过父组件)
### main.js ...... new Vue({ .... beforeCreate(){ Vue.prototype.$bus = this // 安装全局事件总线 } }).$mount('#app') ### App.vue <template> <div id="root"> <div class="todo-container"> <div class="todo-wrap"> ...... <!-- <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo" /> --> <!--不再需要上面那种传数据的方式--> <MyList :todos="todos" /> ...... </div> </div> </div> </template> <script> import MyHeader from'./components/FirstDemo/MyHeader.vue' import MyList from'./components/FirstDemo/MyList.vue' import MyFooter from'./components/FirstDemo/MyFooter.vue' export default { name: 'App', data(){ return { todos:[ ...... ] } }, components: { MyHeader, MyList, MyFooter }, methods:{ addTodo(x){ ...... }, checkTodo(id){ // 回调函数 this.todos.forEach((todo)=>{ if(todo.id==id) todo.done=!todo.done }) }, deleteTodo(id){ // 回调函数 this.todos = this.todos.filter(todo=>todo.id!==id) }, checkAllTodo(done){ ...... }, clearTodoDone(){ ...... } }, mounted(){ // 绑定事件并指定回调函数 this.$bus.$on('checkTodo',this.checkTodo) this.$bus.$on('deleteTodo',this.deleteTodo) } } </script> <style> ...... </style> ### MyList.vue <template> <ul class="todo-main"> <!-- <MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" :checkTodo="checkTodo" :deleteTodo="deleteTodo" /> --> <!--不再使用之前的方式去传数据--> <MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" /> </ul> </template> <script> import MyItem from './MyItem.vue' export default { name:'MyList', data(){ return { ...... } }, components:{MyItem}, // props:['todos','checkTodo','deleteTodo'] // 不再接收父组件传过来的数据 props:['todos'] } </script> ### MyItem.vue <template> ...... </template> <script> export default { name:'MyItem', // props:['todo','checkTodo','deleteTodo'], // 不再接收 props:['todo'], methods:{ handleCheck(id){ // this.checkTodo(id) this.$bus.$emit('checkTodo',id) // 触发事件并传数据参数 }, handDelete(id){ if(confirm('确定删除吗?')){ // this.deleteTodo(id) this.$bus.$emit('deleteTodo',id) // 触发事件并传数据参数 } } } } </script>
消息订阅与发布
-
订阅消息: 消息名
-
发布消息: 消息内容
-
安装第三方"发布消息库"(这种库有很多) ==> pubsub-js
npm i pubsub-js
-
使用方式很简单,一个组件调动api发布,另外一个组件调用api接收即可
-
测试环境如下: School组件和Student组件(演示Student组件给School组件传数据)
### school.vue <template> <div class="demo"> <h3>名称:{{name}}</h3> <h3>地址:{{address}}</h3> </div> </template> <script> export default { name:'School', data(){ return { name:'顶尖', address:'厦门', } }, } </script> <style scoped> .demo { background-color: #08C63E; } </style> ### Student.vue <template> <div class="demo"> <h3>名字:{{name}}</h3> <h3>年龄:{{age}}</h3> </div> </template> <script> export default { name:'Student', data(){ return { name:'Jim Green', age:20 } }, } </script> <style scoped> .demo { background-color: skyblue; } </style>
### Student.vue <template> <div class="demo"> <h3>名字:{{name}}</h3> <h3>年龄:{{age}}</h3> <!--触发逻辑--> <button type="button" @click="sendStudentName">发送学生名字</button> </div> </template> <script> import pubsub from 'pubsub-js' // 导入库 export default { name:'Student', data(){ return { name:'Jim Green', age:20 } }, methods:{ sendStudentName(){ // 发布消息 // 传过去的第一个参数是'事件名称',第二个参数才是 name pubsub.publish('hello',this.name) } } } </script> <style scoped> .demo { background-color: skyblue; } </style> ### School.vue <template> <div class="demo"> <h3>名称:{{name}}</h3> <h3>地址:{{address}}</h3> </div> </template> <script> import pubsub from 'pubsub-js' // 导入库 export default { name:'School', data(){ return { name:'顶尖', address:'厦门', } }, mounted(){ // 自定义pubId变量存储 消息ID,为了是销毁组件解绑消息的时候传参 // 接收消息 this.pubId = pubsub.subscribe('hello',function(msgName,data){ console.log('School','收到发布的消息了,收到的数据是',data) console.log('该消息名称是',msgName) }) }, beforeDestroy(){ // 销毁之前解绑消息 pubsub.unsubscribe(this.pubId) } } </script> <style scoped> .demo { background-color: #08C63E; } </style>
-
this坑演示: 当在vue传入第三方库的时候,this的指向就不明确了
### Schoo.vue ...... mounted(){ this.pubId = pubsub.subscribe('hello',function(msgName,data){ console.log(this) // undefined }) }, - 解决方式: 使用箭头函数/在methods里定义方法 mounted(){ // this.pubId = pubsub.subscribe('hello',function(msgName,data){ // console.log('School','收到发布的消息了,收到的数据是',data) // console.log('该消息名称是',msgName) // console.log(this) // undefined // }) // 修改成箭头函数形式 this.pubId = pubsub.subscribe('hello',(msgName,data)=>{ ...... console.log(this) // vc实例 }) },
消息订阅与发布总结(pubsub)
-
一种组件间通信的方式,适用于任意组件间通信
-
使用步骤
-
安装pubsub: npm i pubsub-js
-
引入: import pubsub from 'pubsub-js'
-
接收数据: A组件想接收数据,则在A组件中订阅消息,订阅的回调函数留在A组件自身
methods(){ demo(data){......} }, mounted(){ this.pubId = pubsub.subscribe('xxx',this.demo) // 订阅消息 }
-
提供数据: pubsub.publish('xxx',数据),注意第一个参数是消息名称,数据在第二个参数
-
最好在beforeDestroy写一个取消订阅的逻辑
mounted(){ this.pubId = pubsub.subscribe('hello',......) }, beforeDestroy(){ pubsub.unsubscribe(this.pubId) }
-
消息订阅运用到todo案例(改造删除功能)
### MyItem.vue
......
<script>
import pubsub from 'pubsub-js' // 引入
export default {
name:'MyItem',
props:['todo'],
methods:{
handleCheck(id){
......
},
handDelete(id){
if(confirm('确定删除吗?')){
// this.$bus.$emit('deleteTodo',id)
pubsub.publish('deleteTodo',id) // 发布消息
}
}
}
}
</script>
### App.vue
<script>
......
import pubsub from 'pubsub-js' // 导入
export default {
name: 'App',
data(){
return {
todos:[
......
]
}
},
components: {
MyHeader,
MyList,
MyFooter
},
methods:{
......
// deleteTodo(id){
// this.todos = this.todos.filter(todo=>todo.id!==id)
// },
deleteTodo(_,id){ // 第一个参数是'事件名',但我们不需要,所以使用'_'代替
this.todos = this.todos.filter(todo=>todo.id!==id)
},
.......
},
mounted(){
// this.$bus.$on('deleteTodo',this.deleteTodo)
pubsub.subscribe('deleteTodo',this.deleteTodo) // 订阅消息
}
}
</script>
todo案例编辑功能的实现
### MyItem.vue
<template>
<li>
<label>
......
<!--根据isEdit的值,要么显示input框,要么显示span-->
<span v-show="!todo.isEdit">{{todo.title}}</span>
<!--多了一个失去焦点事件,:value和使用v-model是一样的效果-->
<input type="text" v-show="todo.isEdit" :value="todo.title" @blur="handBlur(todo,$event)" >
</label>
......
<!--自定义btn-edit样式-->
<!--绑定点击事件,当用户点击'编辑'按钮的时候,'编辑'按钮自动消失(根据需求来)-->
<button class="btn btn-edit" @click="handEdit(todo)" v-show="!todo.isEdit" >编辑</button>
</li>
</template>
<script>
import pubsub from 'pubsub-js'
export default {
name:'MyItem',
props:['todo'],
methods:{
handleCheck(id){
......
},
handDelete(id){
......
}
},
handEdit(todo){
// todo.isEdit = true; // 不是响应式属性
// this.$set(todo,'isEdit',true)
if(todo.hasOwnProperty('isEdit')){
todo.isEdit = true;
}else{
this.$set(todo,'isEdit',true); // 创造响应式属性
}
},
handBlur(todo,event){
todo.isEdit = false;
if(!event.target.value.trim()) return alert('输入不能为空') // 空值判断
// console.log(todo.title) // 打篮球
// console.log(event.target.value) // 打篮球123
this.$bus.$emit('updateTodo',todo.id,event.target.value) // 使用事件总线传值
}
}
}
</script>
<style>
......
</style>
### App.vue
<template>
......
</template>
<script>
......
import pubsub from 'pubsub-js'
export default {
name: 'App',
data(){
return {
todos:[
......
]
}
},
components: {
MyHeader,
MyList,
MyFooter
},
methods:{
......
updateTodo(id,title){ // 接收两个参数
this.todos.forEach(todo=>{
if(todo.id==id) todo.title=title
})
}
},
mounted(){
......
this.$bus.$on('updateTodo',this.updateTodo) // 绑定事件并指定回调
}
}
</script>
<style>
/*base*/
......
.btn-edit { /*自定义样式*/
color: #fff;
background-color: skyblue;
border: 1px solid #bd362f;
margin-right: 5px;
}
......
</style>
todo案例实现功能: 当点击'编辑'按钮的时候,input框立即获得焦点
### MyItem.vue
<template>
<li>
<label>
......
<input type="text"
v-show="todo.isEdit"
v-model="todo.title"
@blur="handBlur(todo,$event)"
ref="inputTitle" > <!--增加ref属性,以便找到这个元素-->
......
</li>
</template>
......
handEdit(todo){
if(......){
......
}else{
......
}
// 不能直接写,因为input中v-show的影响,元素还没有被展示出来
// 既然元素还没有被展示出来,获取焦点肯定是行不通的
// this.$refs.inputTitle.focus()
setInterval(()=>{
this.$refs.inputTitle.focus() // 等待一点时间再获取焦点
},1000)
},
-
官方推荐使用"$nextTick"(开发中经常用到)
- 作用: 下一轮DOM元素被更新的时候,再执行里面的逻辑 - API: 传入一个回调,执行你要的逻辑
...... handEdit(todo){ if(......){ ...... }else{ ...... } // this.$refs.inputTitle.focus() // setInterval(()=>{ // this.$refs.inputTitle.focus() // },1000) this.$nextTick(function(){ this.$refs.inputTitle.focus() // 推荐使用 }) },
-
也有开发者这么搞,同样实现功能(vue中就使用vue推荐的...)
handEdit(todo){ if(......){ ...... }else{ ...... } // this.$refs.inputTitle.focus() // setInterval(()=>{ // this.$refs.inputTitle.focus() // },1000) // this.$nextTick(function(){ // this.$refs.inputTitle.focus() // }) setInterval(()=>{ // 不传入时间 this.$refs.inputTitle.focus() }) },