初始化脚手架
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/cli
:npm 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)传值,在父组件中,通过子组件的属性,将值带过来,在子组件中,声明接收的属性,有三种接收方式:
- 简单声明接收
- 接收同时对数据类型进行限制
- 接收同时对数据类型进行限制,提供默认值,是否必填限制
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-enter
、v-enter-active
、v-enter-to
。
元素离开依次会绑定这些类名:v-leave
、v-leave-active
、v-leave-to
。
有两个实现方案:动画、过渡。
动画:配置v-enter-active
、v-leave-active
。
过渡:使用<transition>
包裹需要过渡的元素,配置v-enter
、v-enter-to
、v-leave
、v-leave-to
,其中v-enter
和v-leave-to
可以写在一起,v-enter-to
和v-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>