3 使用Vue脚手架(Vue CLI)

本文详细介绍了如何使用Vue CLI初始化项目,包括Vue脚手架的配置、组件自定义事件、浏览器本地存储、混入与插件等。通过Todo-list案例展示了组件化编码流程,并探讨了多种组件间通信方式如props、事件总线、消息订阅与发布。同时,讨论了Vue中的nextTick用法及其在优化todolist中的应用。

3 使用Vue脚手架(Vue CLI)

3.1 初始化脚手架

  1. Vue 脚手架是 Vue 官方提供的标准化开发工具(开发平台)。(command line interface)

  2. 最新的版本是 5.x。

  3. 文档: https://cli.vuejs.org/zh/。

淘宝镜像: npm config set registry https://registry.npmmirror.com/

  1. 第一步(仅第一次执行):全局安装@vue/cli。

npm install -g @vue/cli

  1. 第二步:切换到你要创建项目的目录,然后使用命令创建项目

vue create xxxx

  1. 第三步:启动项目

npm run serve yarn serve

  • Vue 脚手架隐藏了所有 webpack 相关的配置,若想查看具体的 webpakck 配置,请执行:vue inspect > output.js

├── 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:包版本控制文件

npm run serve->main.js->app.vue->xxx.vue...先看App的引入再看其配置项->index.html

export default {

​ name:‘MyStudent’, //双驼峰命名,否则报错,但关闭语法检查后不会报错

​ data(){

​ return {

​ name:‘张三’,

​ age:18

​ }

​ }

}

3.1.1 render

在这里插入图片描述

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

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

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

  4. 因为vue.runtime.xxx.js没有模板解析器,所以不能使用template这个配置项,需要使用render函数接收到的createElement函数去指定具体内容。将组件放入容器中

3.1.2 vue.config.js

  1. 使用vue inspect > output.js可以查看到Vue脚手架的默认配置。

  2. 使用vue.config.js可以对脚手架进行个性化定制,详情见:https://cli.vuejs.org/zh

  pages: {
    index: {
      //入口
      entry: 'src/main.js',
    },
  },
	lintOnSave:false, //关闭语法检查

3.2 ref props

3.2.1 ref属性

  1. 被用来给元素子组件注册引用信息(id的替代者)
  2. 应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(vc)
  3. 使用方式:
    1. 打标识:<h1 ref="xxx">.....</h1><School ref="xxx"></School>
    2. 获取:this.$refs.xxx
<template>
	<div>
		<h1 v-text="msg" ref="title"></h1>
		<button ref="btn" @click="showDOM">点我输出上方的DOM元素</button>
		<School ref="sch"/>
	</div>
</template>

<script>
	//引入School组件
	import School from './components/School'

	export default {
		name:'App',
		components:{School},
		data() {
			return {
				msg:'欢迎学习Vue!'
			}
		},
		methods: {
			showDOM(){
				console.log(this.$refs.title) //真实DOM元素
				console.log(this.$refs.btn) //真实DOM元素
				console.log(this.$refs.sch) //School组件的实例对象(vc)
			}
		},
	}
</script>

3.2.2 props配置项

让组件接收外部传过来的数据

  • App.vue
<template>
	<div>
        <!--传number时采用v-bind-->
		<Student name="李四" sex="女" :age="18"/>
	</div>
</template>

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

	export default {
		name:'App',
		components:{Student}
	}
</script>
  • Student.vue
<template>
	<div>
		<h1>{{msg}}</h1>
		<h2>学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
		<h2>学生年龄:{{myAge+1}}</h2>
		<button @click="updateAge">尝试修改收到的年龄</button>
	</div>
</template>

<script>
	export default {
		name:'Student',
		data() {
			console.log(this)
			return {
				msg:'我是一个尚硅谷的学生',
				myAge:this.age
			}
		},
		methods: {
			updateAge(){
				this.myAge++
			}
		},
		//简单声明接收
		 props:['name','age','sex'] 

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

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

==备注:==props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中,然后去修改data中的数据

3.3 mixin混入 插件 scoped

3.3.1 mixin混入

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

  • mixin.js
//定义混入
//分别暴露
export const hunhe = {
	methods: {
		showName(){
			alert(this.name)
		}
	},
	mounted() {//都会执行
		console.log('你好啊!')
	},
}
export const hunhe2 = {
	data() {
		return {
			x:100,
			y:200
		}
	},
}
//所有配置项都可,methods,data有冲突时以组件自身的配置为准  钩子函数都会存在,且先执行混入对象中的钩子函数
  • School.vue
<template>
	<div>
		<h2 @click="showName">学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
	</div>
</template>

<script>
	//引入一个hunhe
	import {hunhe,hunhe2} from '../mixin'

	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷',
				address:'北京',
				x:666
			}
		},
	mixins:[hunhe,hunhe2],//不管引入几个都要写成数组形式
	}
</script>
  • main.js
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'

import {hunhe,hunhe2} from './mixin'
//关闭Vue的生产提示
Vue.config.productionTip = false
//全局混入,vm 和所有vc都会执行
Vue.mixin(hunhe)
Vue.mixin(hunhe2)


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

3.3.2 插件

  1. 功能:用于增强Vue

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

  • 定义插件 plugins.js
export default {//默认暴露
  install(Vue, x, y, z) {
    console.log(x, y, z); //使用者传递的参数 1,2,3
      
    // 1. 添加全局过滤器
    Vue.filter("mySlice", function (value) {
      return value.slice(0, 4);
    });

    // 2. 添加全局指令
    Vue.directive("fbind", {
      //指令与元素成功绑定时(一上来)
      bind(element, binding) {
        element.value = binding.value;
      },
      //指令所在元素被插入页面时
      inserted(element, binding) {
        element.focus();
        console.log(binding);
      },
      //指令所在的模板被重新解析时
      update(element, binding) {
        element.value = binding.value;
      },
    });

    // 3. 配置全局混入(合)
    Vue.mixin({
      data() {
        return {
          x: 100,
          y: 200,
        };
      },
    });

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

//引入插件
import plugins from './plugins'
//应用(使用)插件
Vue.use(plugins,1,2,3)//插件名 传的参数

3.3.3 scoped

  1. 作用:让样式在局部生效,防止冲突。

  2. 写法:<style scoped>

    安装less模块 yarn add less-loader@7(指定版本,webpack版本太低会报错) 还是没找到 执行 npm i less,解决

3.4 Todo-list 案例

  1. 组件化编码流程:

    (1).拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。

    (2).实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:

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

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

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

  1. props适用于:

    (1).父组件 ==> 子组件 通信

    (2).子组件 ==> 父组件 通信(要求父先给子一个函数)

  2. 使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的

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

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'
	import MyList from './components/MyList'
	import MyFooter from './components/MyFooter.vue'

	export default {
		name:'App',
		components:{MyHeader,MyList,MyFooter},
		data() {
			return {
				//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
				todos:[
					{id:'001',title:'抽烟',done:true},
					{id:'002',title:'喝酒',done:false},
					{id:'003',title:'开车',done:true}
				]
			}
		},
		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)=>{
					return !todo.done
				})
			}
		}
	}
</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="请输入你的任务名称,按回车键确认" @keyup.enter="add"  v-model="title"/>
     </div>
</template>

<script>
 import { nanoid } from 'nanoid';
export default {
	name: 'MyHeader',
	props:['addTodo'],
	data() {
		return {
			title: ''
		}
		
	},
	methods: {
		add() {
			//校验输入数据
			if(!this.title.trim()) return alert('输入不能为空')
			//将用户输入包装成一个对象
			const todoObj = { id: nanoid(), title: this.title, done: false }
			//console.log(todoObj);
			//通知APP组件去添加一个对象,addTodo是APP组件传过来的函数
			this.addTodo(todoObj)
			//清空输入框
			this.title =''
		} 

		// add(e) {
		// 	console.log(e.target.value);
		// },
	}
}
</script>

<style scoped>
/*header*/
.todo-header input {
    width: 578px;
    height: 36px;
    font-size: 14px;
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 4px 7px;
	box-sizing: border-box;
	/* box-sizing: border-box; 重要!!!!搞了我一小时呜呜呜 */
	
}

.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 
          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',
    components: { MyItem },
    //声明接收App传递过来的数据,其中todos是自己用的,checkTodo和deleteTodo是给子组件MyItem用的
    props:["todos","checkTodo","deleteTodo"]
  
}
</script>

<style scoped>
/*main*/
.todo-main {
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 1px;
  margin-top: 10px;
}
</style>

MyItem.vue

<template>
    <li>
              <label>
                <input type="checkbox" :checked="todo.done" @change="handelCheck(todo.id)"/>
                <span>{{ todo.title }}</span>
                <!-- 也可以,但不建议,因为改了props,虽然改了对象内部属性vue不会监测(地址值没变)但如果改了地址值,会报错,所以不建议使用 -->
                <!-- <input type="checkbox"  v-model="todo.done"/> 
                  <span>{{ todo.title }}</span>-->
              </label>
              <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
            </li>
</template>

<script>
export default {
  name: 'MyItem',
  //声明接受todo对象
  props: ['todo', "checkTodo",'deleteTodo'],
  methods: {
    //改变CheckBox和todo.done的状态
    handelCheck(id) {
      //checkTodo由APP传到list再传到item
      this.checkTodo(id)
    },
    //删除
    handleDelete(id) {
      if (confirm('确定删除吗?')){
        // console.log(id);
        //通知App组件将对应的todo对象删除
         this.deleteTodo(id)
      }
     
    }
  }
   
}
</script>

<style scoped>
/*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: 7px;
  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: #ddd;
}
li:hover button{
  display: block;
}
</style>

3.5 浏览器本地存储 webStorage

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

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

3.5.1 localStorage

LocalStorage存储的内容,需要手动清除才会消失。(调用api,清空缓存)

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>localStorage</title>
	</head>
	<body>
		<h2>localStorage</h2>
		<button onclick="saveData()">点我保存一个数据</button>
		<button onclick="readData()">点我读取一个数据</button>
		<button onclick="deleteData()">点我删除一个数据</button>
		<button onclick="deleteAllData()">点我清空一个数据</button>

		<script type="text/javascript" >
			let p = {name:'张三',age:18}

			function saveData(){
				localStorage.setItem('msg','hello!!!')
				localStorage.setItem('msg2',666)
				localStorage.setItem('person',JSON.stringify(p))//value不能直接写对象 否则[object object]
			}
			function readData(){
				console.log(localStorage.getItem('msg'))
				console.log(localStorage.getItem('msg2'))

				const result = localStorage.getItem('person')
				console.log(JSON.parse(result))

				console.log(localStorage.getItem('msg3')) // null
			    console.log(JSON.parse(null)) //null
			}
			function deleteData(){
				localStorage.removeItem('msg2')
			}
			function deleteAllData(){
				localStorage.clear()
			}
		</script>
	</body>
</html>

3.5.2 sessionStorage

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

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

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

3.5.3 优化todolist

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

	export default {
		name:'App',
		components:{MyHeader,MyList,MyFooter},
		data() {
			return {
				//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
				todos:JSON.parse(localStorage.getItem('todos')) || [] //初始状态localStorage为null,footer读取todos.length时会报错,所以要加 || []
			}
		},
		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)=>{
					return !todo.done
				})
			}
		},
        //深度监测,勾选状态也能存储,done是对象上的一个属性
		watch: {
			todos:{
				deep:true,
				handler(value){
					localStorage.setItem('todos',JSON.stringify(value))
				}
			}
		},
	}
</script>

3.6 组件自定义事件

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

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

3.6.1 父给子绑定自定义事件

  1. 在父组件中:<Demo @atguigu="test"/><Demo v-on:atguigu="test"/>
<template>
	<div class="app">
		<!-- 通过父组件给子组件(student的实例对象vc)绑定一个自定义事件 实现:子给父传递数据(第一种写法,使用@或v-on) -->
		 <Student v-on:atguigu="getStudentName" @demo="m1"/> 
        <!-- 只触发一次 -->
		 <Student @atguigu.once="getStudentName" @demo="m1"/> 
	</div>
</template>

<script>
	import Student from './components/Student'
    
		export default {
		name:'App',
		components:{Student},
		data() {
			return {
				msg:'你好啊!',
				studentName:''
			}
		},
		methods: {
			getStudentName(name,...params){ //扩展运算符,传递多个参数时用
				console.log('App收到了学生名:',name,params)
				this.studentName = name this.studentName = name 
                //模板中要用name,只能从data props computed中获取,要现在data中定义studentname,用来接收studentname
			} 
		} 
	}
</script>
 

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

<script>
	import Student from './components/Student'
    
		export default {
		name:'App',
		components:{Student},
		data() {
			return {
				msg:'你好啊!',
				studentName:''
			}
		},
		methods: {
			getStudentName(name,...params){ //扩展运算符,传递多个参数时用
				console.log('App收到了学生名:',name,params)
				this.studentName = name
			} 
		},
		mounted() {//App挂载完毕
            //this.$refs.student student的实例是对象
            //更灵活,可以异步
			this.$refs.student.$on('atguigu',this.getStudentName) //绑定自定义事件atguigu
			// this.$refs.student.$once('atguigu',this.getStudentName) //绑定自定义事件(一次性)
            
            //也可以把getStudentName写在这,但不推荐
            //错误写法
           // this.$refs.student.$on('atguigu', function getStudentName(name, ...params){
			// 	console.log('App收到了学生名:', name, params)
			// 	console.log(this);//this是student组件的实例对象,因为是student触发的事件
			//  	this.studentName = name
			// })
            
             //正确 用箭头函数
            this.$refs.student.$on('atguigu',(name, ...params)=>{
                console.log('App收到了学生名:', name, params)
                console.log(this);//this是当前组件的实例对象,箭头函数没有自己的this,向外找,mounted(),this是当前vs
                this.studentName = name
            })
            
		},
	}
</script>
注意:通过this.$refs.xxx.$on('atguigu',回调)绑定自定义事件时,
回调要么配置在methods中 
要么用箭头函数 ,否则this指向会出问题!
  • 触发
sendStudentlName(){
	 //触发Student组件实例身上的atguigu事件
	 this.$emit('atguigu',this.name,666,888,900)//事件名 参数(好多个)
 },
  • 解绑
unbind(){
	 this.$off('atguigu') //解绑一个自定义事件
	 // this.$off(['atguigu','demo']) //解绑多个自定义事件 写在数组里
	 // this.$off() //解绑所有的自定义事件
 },
  • 销毁
death(){
	 this.$destroy() //销毁了当前Student组件的实例,销毁后所有Student实例的自定义事件全都不奏效。原生DOM事件还有效
 }
new Vue({
  el: "#app",
  render: (h) => h(App),
  mounted() { //销毁vm后,所有子组件和自定义事件都销毁
    setTimeout(() => {
      this.$destroy();
    }, 3000);
  },
});

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

<Student ref="student" @click.native="show"/>

3.7 全局事件总线(GlobalEventBus)

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

  2. 安装全局事件总线:

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

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

      methods(){
        demo(data){......}
      }
      ......
      mounted() {
        this.$bus.$on('xxxx',this.demo)
      }
      
    2. 提供数据:this.$bus.$emit('xxxx',数据)

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

//School.vue
mounted() {
	 this.$bus.$on('hello',(data)=>{
	 	console.log('我是School组件,收到了数据',data)
 	})
 },
 beforeDestroy() {
	 this.$bus.$off('hello')
 }

//Student.vue
methods: {
	 sendStudentName(){
		 this.$bus.$emit('hello',this.name)
	 }
 }
  • 父->子->孙,父要得到孙的数据时,不用先传子,再传孙
//App.vue
mounted() {
	 this.$bus.$on('checkTodo',this.checkTodo)
	 this.$bus.$on('deleteTodo',this.deleteTodo)
 },
 beforeDestroy() {
	 this.$bus.$off('checkTodo')
	 this.$bus.$off('deleteTodo')
}

//MyItem.vue		
	//勾选or取消勾选
			handleCheck(id){
				//通知App组件将对应的todo对象的done值取反
				// this.checkTodo(id)
				this.$bus.$emit('checkTodo',id)
			},
			//删除
			handleDelete(id){
				if(confirm('确定删除吗?')){
					//通知App组件将对应的todo对象删除
					// this.deleteTodo(id)
					this.$bus.$emit('deleteTodo',id)
				}
			}

3.8 消息订阅与发布pubsub

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

  2. 使用步骤:

    1. 安装pubsub:npm i pubsub-js

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

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

      methods(){
        demo(msgName,data){......}
      }
      ......
      mounted() {
        this.pid = pubsub.subscribe('xxx',this.demo) //订阅消息
      }
      
    4. 提供数据:pubsub.publish('xxx',数据)

    5. 最好在beforeDestroy钩子中,用PubSub.unsubscribe(pid)去取消订阅。

    //School.vue
    import pubsub from 'pubsub-js'
    
    mounted() {
    			this.pubId = pubsub.subscribe('hello',(msgName,data)=>{
    				console.log(this)
    				// console.log('有人发布了hello消息,hello消息的回调执行了',msgName,data)
    			})
    		},
    		beforeDestroy() {
    			// this.$bus.$off('hello')
    			pubsub.unsubscribe(this.pubId)
    		},
    		
    //Student.vue
    import pubsub from 'pubsub-js'
    methods: {
    			sendStudentName(){
    				// this.$bus.$emit('hello',this.name)
    				pubsub.publish('hello',666)
    			}
    		}
    

3.9 nextTick

语法:this.$nextTick(回调函数)

作用:在下一次 DOM 更新结束后执行其指定的回调。

什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时(获取焦点),要在nextTick所指定的回调函数中执行。

3.9.1 优化todolist

  • main.js
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
	el:'#app',
	render: h => h(App),
	beforeCreate() {
		Vue.prototype.$bus = this
	},
})
  • 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'
	import MyList from './components/MyList'
	import MyFooter from './components/MyFooter'

	export default {
		name:'App',
		components:{MyHeader,MyList,MyFooter},
		data() {
			return {
				//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
				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
			updateTodo(id,title){
				this.todos.forEach((todo)=>{
					if(todo.id === id) todo.title = title
				})
			},
			//删除一个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)=>{
					return !todo.done
				})
			}
		},
		watch: {
			todos:{
				deep:true,
				handler(value){
					localStorage.setItem('todos',JSON.stringify(value))
				}
			}
		},
		mounted() {
			this.$bus.$on('checkTodo',this.checkTodo)
			this.$bus.$on('updateTodo',this.updateTodo)
			this.pubId = pubsub.subscribe('deleteTodo',this.deleteTodo)
		},
		beforeDestroy() {
			this.$bus.$off('checkTodo')
			this.$bus.$off('updateTodo')
			pubsub.unsubscribe(this.pubId)
		},
	}
</script>
  • MyItem.vue
<template>
	<li>
		<label>
			<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
			<!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
			<!-- <input type="checkbox" v-model="todo.done"/> -->
			<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)">删除</button>
		<button v-show="!todo.isEdit" class="btn btn-edit" @click="handleEdit(todo)">编辑</button>
	</li>
</template>

<script>
	import pubsub from 'pubsub-js'
	export default {
		name:'MyItem',
		//声明接收todo
		props:['todo'],
		methods: {
			//勾选or取消勾选
			handleCheck(id){
				//通知App组件将对应的todo对象的done值取反
				// this.checkTodo(id)
				this.$bus.$emit('checkTodo',id)
			},
			//删除
			handleDelete(id){
				if(confirm('确定删除吗?')){
					//通知App组件将对应的todo对象删除
					// this.deleteTodo(id)
					// this.$bus.$emit('deleteTodo',id)
					pubsub.publish('deleteTodo',id)
				}
			},
			//编辑
			handleEdit(todo){
				if(todo.hasOwnProperty.call(todo,'isEdit')){
					todo.isEdit = true
				}else{
					// console.log('@')
					this.$set(todo,'isEdit',true)
				}
				this.$nextTick(function () {//不用$nextTick的话,先获取焦点,后显示input框,没效果
					//在下一次 DOM 更新结束后执行其指定的回调,模板解析完毕后调用
					this.$refs.inputTitle.focus()
				})
			},
			//失去焦点回调(真正执行修改逻辑)
			handleBlur(todo,e){
				todo.isEdit = false
				if(!e.target.value.trim()) return alert('输入不能为空!')
				this.$bus.$emit('updateTodo',todo.id,e.target.value)
			}
		},
	}
</script>

3.10 过渡与动画

  1. 作用:在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名

  2. 在这里插入图片描述

  3. 写法:

    1. 准备好样式:

      • 元素进入的样式:

        1. v-enter:进入的起点
        2. v-enter-active:进入过程中
        3. v-enter-to:进入的终点
      • 元素离开的样式:

        1. v-leave:离开的起点
        2. v-leave-active:离开过程中
        3. leave-to:离开的终点
        动画
        <style scoped>
        	h1{
        		background-color: orange;
        	}
        
        	.hello-enter-active{
        		animation: atguigu 0.5s linear;
        	}
        
        	.hello-leave-active{
        		animation: atguigu 0.5s linear reverse;
        	}
        
        	@keyframes atguigu {
               /* @keyframes 关键帧 */
        		from{
        			transform: translateX(-100%);
        		}
        		to{
        			transform: translateX(0px);
        		}
        	}
        </style>
        
        过渡
        <style scoped>
        	h1{
        		background-color: orange;
        	}
        	/* 进入的起点、离开的终点 */
        	.hello-enter,.hello-leave-to{
        		transform: translateX(-100%);
        	}
        	.hello-enter-active,.hello-leave-active{
        		transition: 0.5s linear;
        	}
        	/* 进入的终点、离开的起点 */
        	.hello-enter-to,.hello-leave{
        		transform: translateX(0);
        	}
        
        </style>
        
    2. 使用<transition>包裹要过度的元素,并配置name属性:

      <transition name="hello" appear>
          <!-- appear 一上来就显示-->
         <h1 v-show="isShow">你好啊!</h1>
      </transition>
      
    3. 备注:若有多个元素需要过度,则需要使用:<transition-group>,且每个元素都要指定key值。

第三方库 npm install animate.css

<template>
	<div>
		<button @click="isShow = !isShow">显示/隐藏</button>
		<transition-group 
			appear
			name="animate__animated animate__bounce" 
			enter-active-class="animate__swing"
			leave-active-class="animate__backOutUp"
		>
			<h1 v-show="!isShow" key="1">你好啊!</h1>
			<h1 v-show="isShow" key="2">尚硅谷!</h1>
		</transition-group>
	</div>
</template>

<script>
	import 'animate.css'
	export default {
		name:'Test',
		data() {
			return {
				isShow:true
			}
		},
	}
</script>

<style scoped>
	h1{
		background-color: orange;
	}
	

</style>

3.10.1 优化todolist

<template>
	<ul class="todo-main">
		<transition-group name="todo" appear>
            <!--一定要写group,因为v-for遍历多个li-->
			<MyItem 
				v-for="todoObj in todos"
				:key="todoObj.id" 
				:todo="todoObj" 
			/>
		</transition-group>
	</ul>
</template>

<script>
	import MyItem from './MyItem'

	export default {
		name:'MyList',
		components:{MyItem},
		//声明接收App传递过来的数据
		props:['todos']
	}
</script>

<style scoped>
	/*main*/
	.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;
	}

	.todo-enter-active{
		animation: atguigu 0.5s linear;
	}

	.todo-leave-active{
		animation: atguigu 0.5s linear reverse;
	}

	@keyframes atguigu {
		from{
			transform: translateX(100%);
		}
		to{
			transform: translateX(0px);
		}
	}
</style>
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值