Vue笔记03-使用Vue脚手架

初始化脚手架

Vue脚手架是Vue官方提供的标准化开发工具,可以方便我们的开发任务。Vue的脚手架叫Vue CLI,CLI是command line interface的简称。
要想使用Vue脚手架,需要全局安装@vue/cli,在安装之前,需要安装node.js。
node.js下载地址
设置淘宝镜像:npm config set registry https://registry.npm.taobao.org
全局安装@vue/clinpm install -g @vue/cli
cmd中跳转到需要创建脚手架的目录,使用vue create hello命令创建项目,需要选择Vue2还是Vue3,这里我们先选择Vue2,之后会提示,选择下载源,一个Yarn,一个NPM,这里我选的NPM。
cd到项目里面,使用npm run serve启动项目,可以看到有两个地址,一个是本机访问地址,一个是同局域网内的访问地址。我们在创建脚手架的时候,会自动下载一个demo项目,此时运行的就是这个demo项目。
退出项目的时候,按两次Ctrl+C即可。
使用VSCode打开刚才创建的hello的项目。
脚手架规定,会先运行main.js。

// 引入Vue和App
import Vue from 'vue'
import App from './App.vue'
// 阻止Vue在启动时生成生产提示
Vue.config.productionTip = false;
// 创建Vue实例,并指定模板为#app,将App组件放入模板
new Vue({
  render: h => h(App),
}).$mount('#app');

App.Vue会引入HelloWorld.vue组件,最后找到index.html中id是app的div。

<!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">
    <!--配置tab小图标-->
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
  	<!--当浏览器不支持js的时候,就会显示noscript标签里的内容-->
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <!--前端模板-->
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

Vue脚手架隐藏了webpack相关配置,通过命令vue inspect > output.js查看具体webpack配置。
刚刚,我们新建的hello项目,主要关注以下几个地方:
/public/index.html:主页面
/src/components:组件目录
/src/App.vue:汇总所有组件
/src/main.js:入口文件
Vue有很多版本,其中vue.js是完整版的Vue,包含核心功能+模板解析器,vue.runtime.xxx.js是精简了模板解析器的Vue,需要使用render()函数接收到的createElement()函数指定具体内容。
在vue-cli3以上版本中,默认开启eslint校验,变量定义后,但是未使用会被认为是错误的,为了避免项目不能正常启动,这里把eslint关掉:找到package.json文件,在rule中加入"no-unused-vars": "off"

ref属性

用来给元素或者子组件注册引用信息,和id类似。
ref属性应用于HTML标签上时,获取的是真实DOM元素。
ref属性应用于子组件上时,获取的是子组件的实例对象。
App.vue

<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组件的实例对象
		}
	},
}
</script>

props配置

用于父组件(App.vue)给子组件(Student.vue)传值,在父组件中,通过子组件的属性,将值带过来,在子组件中,声明接收的属性,有三种接收方式:

  1. 简单声明接收
  2. 接收同时对数据类型进行限制
  3. 接收同时对数据类型进行限制,提供默认值,是否必填限制

App.vue

<template>
	<div>
		<!--在父组件中,将值通过属性的方式写到子组件上-->
		<Student name="李四" sex="" :age="18"/>
		<Student name="张三" sex="" :age="20"/>
	</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}}</h2>
		<button @click="updateAge">尝试修改收到的年龄</button>
	</div>
</template>
<script>
	export default {
		name:'Student',
		data() {
			console.log(this);
			return {
				msg:'我是一个尚硅谷的学生',
				myAge:this.age
			}
		},
		methods: {
			// 接收到的props是不建议修改的,比如age是不建议修改的,可能会引起奇奇怪怪的错误,要想修改age,需要用一个其他变量做修改
			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>

mixin混入

在School.vue组件和Student.vue组件中,都会调用showName方法,都会使用一部分公共的变量,可以使用mixin混入写法。
App.vue

<template>
	<div>
		<School/>
		<hr>
		<Student/>
	</div>
</template>
<script>
	import Schoolfrom './components/School'
	import Student from './components/Student'
	
	export default {
		name:'App',
		components:{School,Student}
	}
</script>

School.vue

<template>
	<div>
		<h2 @click="showName">学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
	</div>
</template>
<script>
	// 引入一个混合
	// import {hunhe,hunhe2} from '../mixin.js'
	
	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷',
				address:'北京',
				x:666
			}
		},
		// mixins:[hunhe,hunhe2],
	}
</script>

Student.vue

<template>
	<div>
		<h2 @click="showName">学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
	</div>
</template>
<script>
	// import {hunhe,hunhe2} from '../mixin.js'
	
	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男'
			}
		},
		// mixins:[hunhe,hunhe2]
	}
</script>

minix.js

export const hunhe = {
	methods: {
		showName(){
			alert(this.name);
		}
	},
	mounted() {
		console.log('你好啊!');
	},
}
export const hunhe2 = {
	data() {
		return {
			x:100,
			y:200
		}
	},
}

main.js

// 引入Vue
import Vue from 'vue'
// 引入App
import App from './App.vue'
import {hunhe,hunhe2} from './mixin.js'
// 关闭Vue的生产提示
Vue.config.productionTip = false;
Vue.mixin(hunhe);
Vue.mixin(hunhe2);
// 创建Vue实例
new Vue({
	el:'#app',
	render: h => h(App)
});

可以看到引入混合有多种方式:import引入、mixins引入。在组件中的x和在mixin中的x,优先使用组件中的,组件中没有的y,使用mixin中的y。
说到这里,再多提一句:ES6的暴露方式。

ES6的暴露方式

ES6有3种暴露方式:多行暴露、统一暴露、默认暴露。

// 多行暴露
// module1.js
export function function1() {
	console.log("function1()");
}
export function function2() {
	console.log("function2()");
}
// main.js
import {function1, function2} from 'module1.js';
// 统一暴露
// module2.js
function function3() {
	console.log("function3()");
}
function function4() {
	console.log("function4()");
}
export {function3, function4};
// main.js
import {function3, function4} from 'module2.js';
// 默认暴露,只允许有一个export default{}
// module3.js
export default {
	function function5() {
		console.log("function5()");
	},
	function function6() {
		console.log("function6()");
	}
}
// main.js
import module3 from 'module3.js';
module3.function5();
module3.function6();

插件

插件是一个包含了install()方法的对象,在插件里,可以定义许多内容,之后在main.js里引入插件,在Vue组件中就可以直接使用了。
plugins.js

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

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

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

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

School.vue

<template>
	<div>
		<h2>学校名称:{{name | mySlice}}</h2>
		<h2>学校地址:{{address}}</h2>
		<button @click="test">点我测试一个hello方法</button>
	</div>
</template>
<script>
	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷atguigu',
				address:'北京',
			}
		},
		methods: {
			test(){
				this.hello();
			}
		},
	}
</script>

Student.vue

<template>
	<div>
		<h2>学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
		<input type="text" v-fbind:value="name">
	</div>
</template>
<script>
	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男'
			}
		}
	}
</script>

main.js

// 引入Vue
import Vue from 'vue'
// 引入App
import App from './App.vue'
// 引入插件
import plugins from './plugins.js'
// 关闭Vue的生产提示
Vue.config.productionTip = false;

// 应用(使用)插件
Vue.use(plugins,1,2,3);
// 创建Vue实例
new Vue({
	el:'#app',
	render: h => h(App)
});

scoped属性

在School.vue组件和Student.vue组件中定义的style信息,在编译后,都汇总到一起,此时,就会出现一个问题,比如School.vue和Student.vue中都给.demo元素定义了样式,但是在两个组件中样式是不同的,汇总之后,就会出现样式覆盖问题,最终的样式取决于,App.vue中后import的组件中style的定义。
为了避免出现这样的情况,就要在style标签上加一个scoped属性,表示这个样式只针对当前组件生效,不会对其他组件中相同选择器的元素起作用。
当编译之后汇总到一起,Vue会给外层div添加一个特殊的标签属性,通过这个标签属性,就可以区分出来哪个组件了,应用样式的时候,就不会混乱了。
Student.vue的style标签

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

School.vue的style标签

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

TodoList案例

首先分析页面结构,将页面分成4个组件:MyFooter.vue、MyHeader.vue、MyItem.vue、MyList.vue,其中MyItem.vue是MyList.vue的子组件。
在App.vue里引入MyFooter.vue、MyHeader.vue、MyList.vue对应的标签,在MyList.vue里引入MyItem.vue标签。对于多个组件都在使用的数据,可以放在公共的组件里的定义,比如这里的todos数据,在MyFooter.vue、MyHeader.vue、MyList.vue中都有使用,所以todos放在App.vue中。采取一个原则:数据在哪里,处理数据的方法就在哪里,所以还要在App.vue里定义几个操作todos数据的方法。数据的传输和方法的传输,通过自定义组件的属性进行传递。这里有一个原则,props接收到的数据,不要修改,因为Vue不建议这么操作,因此v-model绑定的值不要是props传过来的值。
新增的todo中,id属性来自nanoid,这个需要通过npm i nanoid命令安装。
以当前学到的技术,在Vue各组件之间传递数据或者方法的时候,是通过组件上的属性传递的,可以由父传给子,也可以由子传给父,但是不能随意传递,如果中间还有层级,需要一级一级的传递,后面会介绍跟高级的写法。
父传给子,需要把数据写到子标签的属性上,子组件通过props接收。
子传给父,需要在父组件中提前写好一个方法,将方法绑定在子组件的属性上,子组件通过props接收,在子组件中,调用父组件的方法,把值通过方法传给父组件,在父组件中操作父组件中的data。
在统计已完成的时候,还用到了计算属性的知识。通过reduce()方法进行计算。reduce()方法接收两个参数,第一个参数是一个计算表达式,第二个参数是一个初始值,关于reduce()方法,可以查看这里
props传过来的值如果是对象类型,修改对象中的属性不会报错,但是Vue不推荐这样做。
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)
});

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是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>

MyFooter.vue

<template>
	<div class="todo-footer" v-show="total">
		<label>
			<!-- <input type="checkbox" :checked="isAll" @change="checkAll"/> -->
			<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: {
			//总数
			total(){
				return this.todos.length
			},
			//已完成数
			doneTotal(){
				//此处使用reduce方法做条件统计
				/* const x = this.todos.reduce((pre,current)=>{
					console.log('@',pre,current)
					return pre + (current.done ? 1 : 0)
				},0) */
				//简写
				return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0);
			},
			//控制全选框
			isAll:{
				//全选框是否勾选
				get(){
					return this.doneTotal === this.total && this.total > 0;
				},
				//isAll被修改时set被调用
				set(value){
					this.checkAllTodo(value);
				}
			}
		},
		methods: {
			/* checkAll(e){
				this.checkAllTodo(e.target.checked)
			} */
			//清空所有已完成
			clearAll(){
				this.clearAllTodo();
			}
		},
	}
</script>

MyHeader.vue

<template>
	<div class="todo-header">
		<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
	</div>
</template>
<script>
	import {nanoid} from 'nanoid'
	export default {
		name:'MyHeader',
		//接收从App传递过来的addTodo
		props:['addTodo'],
		data() {
			return {
				//收集用户输入的title
				title:''
			}
		},
		methods: {
			add(){
				//校验数据
				if(!this.title.trim()) return alert('输入不能为空');
				//将用户的输入包装成一个todo对象
				const todoObj = {id:nanoid(),title:this.title,done:false};
				//通知App组件去添加一个todo对象
				this.addTodo(todoObj);
				//清空输入
				this.title = '';
			}
		},
	}
</script>

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>

MyItem.vue

<template>
	<li>
		<label>
			<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
			<!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
			<!-- <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、checkTodo、deleteTodo
		props:['todo','checkTodo','deleteTodo'],
		methods: {
			//勾选or取消勾选
			handleCheck(id){
				//通知App组件将对应的todo对象的done值取反
				this.checkTodo(id);
			},
			//删除
			handleDelete(id){
				if(confirm('确定删除吗?')){
					//通知App组件将对应的todo对象删除
					this.deleteTodo(id);
				}
			}
		},
	}
</script>

浏览器本地存储

浏览器的本地存储有localStorage和sessionStorage两种。它们的API是一样的,不同点在于存储内容的失效时间。
localStorage需要手动清理才会消失,sessionStorage随着窗口的关闭,自动消失,即sessionStorage是随着会话的关闭而删除的。
xxxStorage.setItem(key, value):将key-value存储到本地存储
xxxStorage.getItem(key):在本地存储中查找key对应的value,取不到的时候,返回null
xxxStorage.removeItem(key):在本地存储中将key-value删除
xxxStorage.clear():清空本地存储中的数据
本地存储一般是5MB,不同浏览器略有区别。

Vue中自定义事件

我们常用的click事件、change事件等,都是js的内置事件,在Vue中可以自定义事件。
自定义事件有两种写法:使用@v-on、使用ref属性。
App.vue

<template>
	<div class="app">
		<h1>{{msg}},学生姓名是:{{studentName}}</h1>
		<!-- 通过父组件给子组件传递函数类型的props实现:子给父传递数据 -->
		<School :getSchoolName="getSchoolName"/>
		<!-- 通过父组件给子组件绑定一个自定义事件,事件名称是“atguigu”,“atguigu”的事件回调是“getStudentName”,子给父传递数据(第一种写法,使用@或v-on),同时,还绑定了一个“demo”事件-->
		<Student @atguigu="getStudentName" @demo="m1"/>
		<!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,使用ref)-->
		<Student ref="student"/>
		<!--绑定在自定义组件上的事件,如果没有native修饰,即使是原生事件,也会被看做自定义事件,click的时候,不会触发,需要添加native修饰-->
		<Student @click.native="show"/>
	</div>
</template>
<script>
	import Student from './components/Student.vue'
	import School from './components/School.vue'

	export default {
		name:'App',
		components:{School,Student},
		data() {
			return {
				msg:'你好啊!',
				studentName:''
			}
		},
		methods: {
			// 在School.vue中调用getSchoolName()方法并传递一个参数
			getSchoolName(name){
				console.log('App收到了学校名:', name);
			},
			// 在Student.vue中调用getStudentName()方法并传递一个参数,这里的...params是可变参数
			getStudentName(name,...params){
				console.log('App收到了学生名:', name, params);
				this.studentName = name;
			},
			m1(){
				console.log('demo事件被触发了!');
			},
			show(){
				alert(123);
			}
		},
		mounted() {
			// 这种写法的优势是可以异步绑定事件,写一个setTimeOut()即可
			this.$refs.student.$on('atguigu',this.getStudentName);// 绑定自定义事件
			// 将上一句的this.getStudentName直接拿过来,作用是不一样的,注意看this.studentName = name;
			// 这里的this是指Student对象,而非App对象,所以不生效,Vue规定这里的this是触发事件的对象,如果这里写成箭头函数是生效的
			this.$refs.student.$on('atguigu', function(name, ...params) {
				console.log('App收到了学生名:',name,params);
				this.studentName = name;
			});// 绑定自定义事件,会有问题
			// this.$refs.student.$once('atguigu',this.getStudentName);// 绑定自定义事件(一次性)
		},
	}
</script>

School.vue

<template>
	<div class="school">
		<h2>学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
		<button @click="sendSchoolName">把学校名给App</button>
	</div>
</template>
<script>
	export default {
		name:'School',
		// 子组件接收父组件传给的参数
		props:['getSchoolName'],
		data() {
			return {
				name:'尚硅谷',
				address:'北京',
			}
		},
		methods: {
			sendSchoolName(){
				// 调用App.vue中定义的getSchoolName()方法,并把子组件的值传给父组件
				this.getSchoolName(this.name);
			}
		},
	}
</script>

Student.vue

<template>
	<div class="student">
		<h2>学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
		<h2>当前求和为:{{number}}</h2>
		<button @click="add">点我number++</button>
		<button @click="sendStudentlName">把学生名给App</button>
		<button @click="unbind">解绑atguigu事件</button>
		<button @click="death">销毁当前Student组件的实例(vc)</button>
	</div>
</template>
<script>
	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男',
				number:0
			}
		},
		methods: {
			add(){
				console.log('add回调被调用了');
				this.number++;
			},
			sendStudentlName(){
				// 触发Student组件实例身上的atguigu事件
				this.$emit('atguigu',this.name,666,888,900);
				// this.$emit('demo');
				// this.$emit('click');
			},
			unbind(){
				this.$off('atguigu');// 解绑一个自定义事件
				// this.$off(['atguigu','demo']);// 解绑多个自定义事件
				// this.$off();// 解绑所有的自定义事件
			},
			death(){
				this.$destroy();//;销毁了当前Student组件的实例,销毁后所有Student实例的自定义事件全都不奏效
			}
		},
	}
</script>

使用$emit()触发自定义事件,使用$off()解绑自定义事件,使用$destroy()销毁组件,一旦组件被销毁,组件上绑定的事件都将失效,Vue的响应式也将失效,但是原生的dom事件是不受影响的。

全局事件总线

为了解决不同组件间数据传递的问题,引出全局事件总线的概念。假设A需要向B传输数据,这时候,就需要一个中间人C,B向C注册(使用$on())一个自定义事件,自定义事件的回调方法在B中,在A中触发(使用$emit())C上的自定义事件,并将C的数据传给C的回调函数,此时数据就从A经过C传到了B。
那么任意两个组件间都可以依赖这个中间人C来实现数据的传递,那么中间人C就要求,对所有组件可见,C上可以调用$on()绑定事件,可以调用$emit()触发事件。
这个C最终选在Vue的原型对象上,并且在beforeCreate()的时候,绑定到Vue的原型对象上,这样在创建Vue实例的时候,就可以看到这个C了。
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;// 安装全局事件总线
	},
});

接收数据的组件(School.vue)给$bus上绑定事件

<template>
	<div class="school">
		<h2>学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
	</div>
</template>
<script>
	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷',
				address:'北京',
			}
		},
		mounted() {
			// console.log('School',this)
			this.$bus.$on('hello',(data)=>{
				console.log('我是School组件,收到了数据',data);
			});
		},
		beforeDestroy() {
			this.$bus.$off('hello');
		},
	}
</script>

发送数据的组件(Student.vue)让$bus触发事件

<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:'男',
			}
		},
		mounted() {
			// console.log('Student',this.x)
		},
		methods: {
			sendStudentName(){
				this.$bus.$emit('hello',this.name);
			}
		},
	}
</script>

有一点需要注意,$bus是一个独立的VueComponent,如果其他组件给$bus上注册了事件,其他组件在执行beforeDestroy()方法时候,最好使用$off()来解绑在$bus上的事件。

消息订阅与发布

和全局事件总线类似,可以引入第三方的js来实现各个组件互相传值,这里推荐一个pubsub-js,使用npm i pubsub-js来安装。
需要接收数据的组件使用pubsub订阅,需要发送数据的组件使用pubsub发送。
School.vue中javascript部分

import pubsub from 'pubsub-js'
export default {
	name:'School',
	data() {
		return {
			name:'尚硅谷',
			address:'北京',
		}
	},
	mounted() {
		// console.log('School',this);
		/* this.$bus.$on('hello',(data)=>{
			console.log('我是School组件,收到了数据',data);
		}) */
		// 注册监听
		this.pubId = pubsub.subscribe('hello',(msgName,data)=>{
			// 要想使得这里的this是Vue实例,需要写成箭头函数,否则,就需要调用一个写在method里的函数
			// console.log(this);
			console.log('有人发布了hello消息,hello消息的回调执行了',msgName,data);
		});
	},
	beforeDestroy() {
		// this.$bus.$off('hello');
		// 取消监听的时候使用id取消
		pubsub.unsubscribe(this.pubId);
	},
}

Student.vue中javascript部分

import pubsub from 'pubsub-js'
export default {
	name:'Student',
	data() {
		return {
			name:'张三',
			sex:'男',
		}
	},
	methods: {
		sendStudentName(){
			// this.$bus.$emit('hello',this.name);
			// 发布消息
			pubsub.publish('hello',666);
		}
	},
}

$nextTick

回到TodoList案例,给MyItem.vue添加一个编辑按钮,点击编辑按钮的时候,内容变成input框,并且焦点聚焦在input框上,修改数据,input框失去焦点的时候进行保存。
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失去焦点的时候,调用handleBlur()方法-->
			<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>
		<!--点击“编辑”触发handleEdit()方法修改isEdit的值-->
		<!--isEdit的值影响input的显示-->
		<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('isEdit')){
					todo.isEdit = true;
				}else{
					// console.log('@');
					this.$set(todo,'isEdit',true);
				}
				// 直接使用this.$refs.inputTitle.focus();不生效的原因
				// handleEdit()方法执行完成之后,会重新渲染DOM元素,新生成的DOM的聚焦就丢失了,要想实现focus(),需要在DOM渲染之后再执行才生效
				// $nextTick的意思是,在下一轮DOM刷新后执行回调方法
				// 当数据改变后,需要基于更新后的新DOM进行操作时候,使用$nextTick
				this.$nextTick(function(){
					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>

动画效果

Vue的动画实现原理:在合适的时间,给元素加上样式类名,从而实现动画效果。
元素进入依次会绑定这些类名:v-enterv-enter-activev-enter-to
元素离开依次会绑定这些类名:v-leavev-leave-activev-leave-to
有两个实现方案:动画、过渡。
动画:配置v-enter-activev-leave-active
过渡:使用<transition>包裹需要过渡的元素,配置v-enterv-enter-tov-leavev-leave-to,其中v-enterv-leave-to可以写在一起,v-enter-tov-leave写在一起,如果包裹的元素没有name,那么后面就写v-,如果元素有name,就需要把v-换成name-才生效。
如果<transition>包裹了多个元素,需要将<transition>换成<transition-group>并给包裹的元素指定一个key值。

动画

<template>
	<div>
		<button @click="isShow = !isShow">显示/隐藏</button>
		<!--appear:第一次出现的时候就加载动画效果-->
		<transition name="hello" appear>
			<h1 v-show="isShow">你好啊!</h1>
		</transition>
	</div>
</template>
<script>
	export default {
		name:'Test',
		data() {
			return {
				isShow:true
			}
		}
	}
</script>
<style scoped>
	h1{
		background-color: orange;
	}
	/*因为元素有name,所以使用name前缀,如果没有指定name,使用v前缀*/
	/*指定enter时候的时间和速度*/
	.hello-enter-active{
		animation: atguigu 0.5s linear;
	}
	/*因为元素有name,所以使用name前缀,如果没有指定name,使用v前缀*/
	/*指定leave时候的时间和速度,reverse代表反向*/
	.hello-leave-active{
		animation: atguigu 0.5s linear reverse;
	}
	/*定义atguigu动画,指定动画的from和to,即开始和结束的位置*/
	@keyframes atguigu {
		from{
			transform: translateX(-100%);
		}
		to{
			transform: translateX(0px);
		}
	}
</style>

过渡

<template>
	<div>
		<button @click="isShow = !isShow">显示/隐藏</button>
		<!--transition包裹多个元素的时候换成transition-group,并给多个元素指定key-->
		<transition-group name="hello" appear>
			<h1 v-show="!isShow" key="1">你好啊!</h1>
			<h1 v-show="isShow" key="2">尚硅谷!</h1>
		</transition-group>
	</div>
</template>
<script>
	export default {
		name:'Test',
		data() {
			return {
				isShow:true
			}
		},
	}
</script>
<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>

第三方动画

这里推荐的是Animate.css,需要使用npm install animate.css安装上。

<template>
	<div>
		<button @click="isShow = !isShow">显示/隐藏</button>
		<!--把animate__animated animate__bounce加到name上-->
		<!--把具体的动画类名,加到enter-active-class和leave-active-class上-->
		<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>
	// 引入animate.css
	import 'animate.css'
	export default {
		name:'Test',
		data() {
			return {
				isShow:true
			}
		}
	}
</script>
<style scoped>
	h1{
		background-color: orange;
	}
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值