Vue2 - 脚手架
1. 初始化脚手架
参考地址:https://cli.vuejs.org/zh/
首先你得要有Node.js的环境,有了环境之后,你就可以使用npm命令了。
给npm下载地址配置为淘宝镜像:
npm config set registry https://registry.npm.taobao.org
之后全局安装Vue-CLI:
npm install -g @vue/cli
查看是否可以使用vue命令:
使用Vue-CLI来创建项目:
vue create projectName
注意:这里执行CMD命令的时候,必须要在你想要的目录下进行。
之后就跟着选择,等待安装即可:
2. 模板项目的目录结构
对于刚刚使用Vue-CLI创建的vue-hello-world的基本目录结构:
所有Vue初始化创建出来的都是这样。
├── node_modules
├── public
│ ├── favicon.ico: 页签图标
│ └── index.html: 主页面
├── src
│ ├── assets: 存放静态资源
│ │ └── logo.png
│ │── component: 存放组件
│ │ └── HelloWorld.vue
│ │── App.vue: 汇总所有组件
│ │── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件 ES6=>ES5所需
├── package.json: 应用包配置文件
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件
3. 目录结构分析
对于上面的目录结构,我们来进行详细分析
3.1 入口文件
整个Vue项目的入口文件就是main.js
:
//引入Vue,这样就无需再在html页面中使用script标签来引入了
import Vue from 'vue'
//引入一人之下的存在 APP
import App from './App.vue'
//关闭Vue的生成提示
Vue.config.productionTip = false
//创建Vue实例
new Vue({
render: h => h(App), //暂时不分析
}).$mount('#app') //使用$mount的方式来指定为为哪个容器服务,以前是使用el配置属性来指定
可见Vue实例代码中,出现了我们无法理解的一段代码:
render: h => h(App)
这里我们先不分析这段代码,其他我们都能够看明白。
3.2 App
接下来看App组件,从项目目录上来看,App组件是与入口文件平级的,可见其重要性:
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App', //指定组件名称
components: {
HelloWorld //组成HelloWorld组件
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
可见上面的代码我们都很熟悉了。
3.3 组件文件
对于其他所有的组件,以后我们都会写在components文件夹下,例如上面提到的HelloWorld组件:
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: { //不理解的配置项
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>
/*样式*/
</style>
可见上面也出现了我们之前从未见过的配置项:props
,这个配置项后面再说。
3.4 静态资源
以后所有的静态资源都会存放在assets文件夹下。包括图片,音频等。
3.5 页面呈现
对于整个Vue项目,肯定是需要有一个html
文件,作为整体的页面呈现,所以我们一般也把Vue项目称为单页面应用。
存放的地方是在public文件夹下。
这里的<%= BASE_URL %>
其实就代表我们以前写的./
表示当前目录下的xxxx,这里的<%= BASE_URL %>
准确的来说是表示public
层级下的xxx。
4. render函数
render
函数出现在main.js
中的创建Vue实例的时候:
new Vue({
render: h => h(App),
}).$mount('#app')
在讲解这个render
函数之前,我先将第二章最后一个案例搬过来,使用Vue工程来做做:
School.vue
:<template> <div class="demo"> <h2>学校名称:{{ name }}</h2> <h2>学校地址:{{ address }}</h2> <button @click="showName">点我提示学校名</button> <student></student> </div> </template> <script> import Student from "@/components/Student.vue"; export default { // eslint-disable-next-line vue/multi-word-component-names name: "School", data() { return { name: 'B站大学', address: 'www.bilibili.com' } }, methods: { showName() { alert(this.name) } }, components:{ // eslint-disable-next-line vue/no-unused-components Student } } </script> <style> .demo { background-color: orange; } </style>
Student.vue
:
<template>
<div>
<h2>学生姓名:{{name}}</h2>
<h2>学生年龄:{{age}}</h2>
</div>
</template>
<script>
export default {
// eslint-disable-next-line vue/multi-word-component-names
name:'Student',
data(){
return {
name:'张三',
age:18
}
}
};
</script>
<style>
/*里面写以前的css样式*/
</style>
App.vue
:<template> <div id="app"> <img alt="Vue logo" src="./assets/logo.png"> <school></school> </div> </template> <script> import School from "@/components/School.vue"; export default { name: 'App', components: { School } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
main.js
:import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false new Vue({ render: h => h(App), }).$mount('#app')
index.html
:<!DOCTYPE html> <html lang=""> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <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>
运行结果:
现在我将改变main.js的代码,将他改为之前原始的写法:
new Vue({
el:'#app',
components:{
App
}
/*render: h => h(App),*/
})
查看:
发现报错了,报错简单来说就是,模板编译器不可用,你要么使用预编译的render函数,要么使用包含模板编译器的Vue。
看到上图中有很多的Vue.js,其中Vue.js是最全面的,其他的都是在此基础上进行删删减减的。
补充:
关于不同版本的Vue:
vue.js
与vue.runtime.xxx.js
的区别:
(1).vue.js
是完整版的Vue
,包含:核心功能+模板解析器。
(2).vue.runtime.xxx.js
是运行版的Vue
,只包含:核心功能;没有模板解析器。因为
vue.runtime.xxx.js
没有模板解析器,所以不能使用template
配置项,需要使用render
函数接收到的createElement
函数去指定具体内容。
这里的解决办法:
- 使用带有模板编译器的Vue.js文件
- 使用render函数
这里我使用render函数来解决,看看原始的render函数:
new Vue({
el:'#app',
components:{
App
},
/*render: h => h(App),*/
render(){
console.log(123);
return null
}
})
可见不再报错了。
其实render函数里面还能接收一个参数:createElement
可见它是一个函数,这个函数其实就是JavaScript中创建DOM节点的函数,例如你可以写一个h1标签,然后写上标签体内容:
render(createElement){
console.log(createElement('h1','你好呀'));
return createElement('h1','你好呀')
}
所以,我们在来改造一下,将组件传递进去:
render(createElement){
console.log(createElement(App));
return createElement(App)
}
可见又成功渲染了。
由于我们不需要在render函数里面使用this,所以我们又可以改造为箭头函数:
render: createElement => createElement(App),
这样你就大致明白了render函数了吧。
5. 修改默认配置
本小节主要讲解一下如何修改Vue的默认配置。
在之前我们的代码结构如下:
这里面,我们之前说到,main.js
就是Vue的入口文件,那么它凭什么作为入口文件呢?我用 entry.js
作为入口文件难道不行吗?我们来试试看
执行npm run serve
:
可见报错。
那我现在有个需求,我就想改它应该怎么办呢?使用vue.config.js
来解决
再次启动成功。
补充:
这里还有很多配置,可以自行查看
6. ref属性
我现在有这样一段代码:
App.vue
:
<template>
<div>
<h1 v-text="msg" id="title"></h1>
<button @click="showDOM">点我输出上方的DOM元素</button>
<School/>
</div>
</template>
<script>
import School from "@/components/School.vue";
export default {
name: 'App',
components: {
School
},
data(){
return {
msg:'ref属性知识'
}
},
methods:{
showDOM(){
//
}
}
}
</script>
现在我要求点击按钮,输出DOM元素。
使用以前的写法:
showDOM(){
console.log(document.querySelector('#title'))
}
以上代码虽然能够获取到DOM元素,但是在Vue中,Vue却不希望你这么做,所以,在Vue中Vue希望你使用ref属性来完成:
App.vue
:
<template>
<div>
<h1 v-text="msg" ref="title"></h1>
<button @click="showDOM">点我输出上方的DOM元素</button>
<School/>
</div>
</template>
<script>
import School from "@/components/School.vue";
export default {
name: 'App',
components: {
School
},
data(){
return {
msg:'ref属性知识'
}
},
methods:{
showDOM(){
console.log(this)
}
}
}
</script>
可见我在h1中添加了属性ref, 然后输出this看看:
可见这里获取到了h1。
showDOM(){
console.log(this.$refs.title)
}
可见以后在Vue中想要获取DOM元素,可以直接使用ref了,无需在亲自去操作DOM。
Vue中的标签属性ref其实还能够作用在组件上,我们来看看:
App.vue
:
<template>
<div>
<h1 v-text="msg" ref="title"></h1>
<button @click="showDOM">点我输出上方的DOM元素</button>
<School ref="sch"/>
</div>
</template>
<script>
import School from "@/components/School.vue";
export default {
name: 'App',
components: {
School
},
data(){
return {
msg:'ref属性知识'
}
},
methods:{
showDOM(){
console.log(this)
console.log(this.$refs.sch)
}
}
}
</script>
可见,如果作用到了组件上,那么你获取到的就是整个组件实例对象。
总结:
- 被用来给元素或子组件注册引用信息(id的替代者)
- 应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(
VueComponent
) - 使用方式:
- 打标识:
<h1 ref="xxx">.....</h1>
或<School ref="xxx"></School>
- 获取:```this.$refs.xxx``
- 打标识:
7. Props配置
Props配置主要是用来向子组件传递数据的。
例如现在我有如下代码:
Student.vue
:
<template>
<div>
<h1>{{msg}}</h1>
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<h2>学生年龄:{{age}}</h2>
</div>
</template>
<script>
export default {
name:'Student',
data(){
return {
msg:'props配置学习',
name:'念心卓',
sex:'男',
age:18
}
}
};
</script>
App.vue
:
<template>
<div>
<Student/>
</div>
</template>
<script>
import Student from "@/components/Student.vue";
export default {
name: 'App',
components: {
Student
}
}
</script>
现在有了组件,相信大家都明白服用的道理,现在我要服用Student组件,但是里面的数据可不是上图中的执行结果,我要求数据不一样。
相信大家第一步就会这样改:
App.vue
部分修改:
<template>
<div>
<Student/>
<Student/>
</div>
</template>
但是细心的你还是会发现,其实两者的数据是一样的,只不过你使用了两个Student组件标签,对应着不同的组件实例对象罢了,但是他们的数据却是相同的。那如何做到二者数据不同呢?这时候你可能会想到,我在定义Student
的时候,不直接使用data()
函数来构造数据,而是在使用这个组件的时候,你需要什么样的数据我传递给你即可,所以,这就要引出我们的props配置了,例如下面的代码:
Student.vue
修改:
<template>
<div>
<h1>{{msg}}</h1>
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<h2>学生年龄:{{age}}</h2>
</div>
</template>
<script>
export default {
name:'Student',
data(){
return {
msg:'props配置学习'
}
}
};
</script>
可见在Student中的data()
函数中,我们不再声明要改变的数据(动态数据),但是我们使用依旧是正常使用,但是如果直接这样执行的话,会导致控制台报错,因为根本就没有name、age、sex这几个动态数据
所以,我们要在调用Student组件的时候传入这几个动态数据:
App.vue
修改:
<template>
<div>
<Student name="念心卓" sex="男" age="18"/>
<hr/>
<Student name="王老五" sex="男" age="20"/>
</div>
</template>
<script>
import Student from "@/components/Student.vue";
export default {
name: 'App',
components: {
Student
}
}
</script>
可见我在Student标签上传递了要使用的动态数据,但是这里要注意:必须要在Student处使用props配置来接收才能够使用,就好比微信转账一样,一个转账,一个接收。
继续修改Student.vue:
<template>
<div>
<h1>{{msg}}</h1>
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<h2>学生年龄:{{age}}</h2>
</div>
</template>
<script>
export default {
name:'Student',
data(){
return {
msg:'props配置学习'
}
},
props:['name','sex','age'] //使用props配置来接收父组件传递过来的数据
};
</script>
使用props配置来接收数据,里面的内容为传递过来的key。
可见现在目的达到。
这里还有几个注意点:
在组件中传递数据,传递过来的都是字符串
例如上面代码,对于age数据:
可见都是字符串,那要如果获取到Number类型的数据呢?
使用以前的v-bind即可:
<Student name="念心卓" sex="男" :age="18"/>
;因为v-bind会解析标签属性中的JavaScript表达式,会忽略外侧的引号。可见此刻是Number类型的。
props配置有3中写法
最简单的数组写法,也就是上的这种
props:['key1','key2'...]
对象的写法,接收的同时对数据进行类型限制
props: { name: String, sex: String, age: Number, }
这种写法为
key:数据类型
最完整写法,接收的同时对数据:进行类型限制+默认值的指定+必要性的限制
props: { name: { type: String, required: true }, sex: { type: String, required: true }, age: { type: Number, default: 18 } }
使用type子属性限制类型,required子属下限制是否是必须传递的数据,default子属性当没有传递数据的时候使用默认值
可见当你指定数据类型之后,如果传递的是非Number类型,会报错的。
前提是之前我将v-bind的地方改回去了。
不建议直接更改props接收到的数据
例如我针对
Student.vue
代码:<template> <div> <h1>{{ msg }}</h1> <h2>学生姓名:{{ name }}</h2> <h2>学生性别:{{ sex }}</h2> <h2>学生年龄:{{ age }}</h2> <button @click="updateAge">尝试修改收到的年龄</button> </div> </template> <script> export default { name: 'Student', data() { return { msg: 'props配置学习' } }, methods:{ updateAge(){ this.age++ } }, props: { name: { type: String, required: true }, sex: { type: String, required: true }, age: { type: Number, default: 18 }, } }; </script>
可见我使用updateAge()函数来修改接收到的age属性,结果:
注意:这里不能够监视引用类的数据是否改变,但是能监视引用的改变
那么如果却是想要改怎么办呢?那只有在data()函数中构建数据之后,再操作data()函数中构建的数据
data() { return { msg: 'props配置学习', myAge:this.age } }, methods:{ updateAge(){ this.myAge++ } },
注意,你这里使用了myAge,所以你的插值语法处也要用这个。
当data()构建数据中有与props接收的数据中重名的时候,会优先使用props接收到的数据,因为vue工作的时候,如果有props配置,会先加载props中的配置,再执行data()函数
8. mixin混入
官方文档:https://v2.cn.vuejs.org/v2/guide/mixins.html#%E5%9F%BA%E7%A1%80
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
现在我有如下代码:
Student.vue
:
<template>
<div>
<h2>学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
<button @click="showName">点我输出学生姓名</button>
</div>
</template>
<script>
export default {
name: 'Student',
data() {
return {
name: '念心卓',
sex: '男'
}
},
methods:{
showName(){
console.log(this.name)
}
}
}
</script>
School.vue
:
<template>
<div>
<h2>学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
<button @click="showName">点我输出学校名称</button>
</div>
</template>
<script>
export default {
name: 'School',
data() {
return {
name: 'B站大学',
address: 'www.bilibili.com',
}
},
methods:{
showName(){
console.log(this.name)
}
}
}
</script>
点击按钮分别输出对于的名称。
仔细观察上面的代码,发现methods
中的代码都一样,那么是否可以提出来,然后需要的地方引入呢?这其实就是mixin混入的思想。
实现mixin
(混入步骤):
编写将要混入的代码抽出来,存入
js
文件中例如我将上面的输出名称的方法抽取出来,封装到
mixin.js
文件中export const hunru = { methods: { showName(){ console.log(this.name) } }, }
这里我采用分别暴露的方式,因为可能会有多个对象要被暴露出去
重要点:你之前的代码怎么写的,这里就怎么写,只不过需要暴露出去,而且里面的this,如果在组件中引用这个js,那么this就是这个组件实例对象,如果是在vm中引入这个js,那么这个this就是Vue实例vm
目录结构:
将之前的写的methods删除,新增mixin配置项,用数组接收
Student.vue
:import {hunru} from "@/mixin"; export default { name: 'Student', data() { return { name: '念心卓', sex: '男' } }, mixins:[hunru] }
School.vue
:import {hunru} from "@/mixin"; export default { name: 'School', data() { return { name: 'B站大学', address: 'www.bilibili.com', } }, mixins: [hunru] }
注意,因为使用了分别暴露,所以引入js的时候就要使用
{}
的形式,多个暴露的对象用逗号分割。
最后测试仍然成功。
对于mixin混入,不仅可以使用到methods,还可以用到生命周期函数,以及数据构造data()函数上:
mixin.js
:
export const hunru = {
methods: {
showName() {
console.log(this.name)
}
},
}
export const data = {
data() {
return {
x:100,
y:200
}
}
}
export const lifeCycle = {
mounted() {
console.log('我是mixin内的生命周期函数')
}
}
School.vue:
import {hunru,data,lifeCycle} from "@/mixin";
export default {
name: 'School',
data() {
return {
name: 'B站大学',
address: 'www.bilibili.com',
}
},
mixins: [hunru,data,lifeCycle],
mounted() {
console.log('我是组件内的生命周期函数')
}
}
可见对于生命周期函数来说,mixin.js文件内的生命周期和组件内的生命周期函数都执行了,并且是mixin中的先的先执行。
对于数据构造data()函数来说,它其实也是混入了,相当于合并了,如果组件内的data函数与mixin.js文件内定义的数据有重复的key,那么会以组件内的为主。
注意:
上诉代码的方式都是局部混入:mixins:['xxx']
;如果要实现全局混入:Vue.mixin(xxx)
main.js
:
import Vue from 'vue'
import App from './App.vue'
import {data,hunru,lifeCycle} from "@/mixin";
Vue.config.productionTip = false
Vue.mixin(data)
Vue.mixin(hunru)
Vue.mixin(lifeCycle)
new Vue({
components:{
App
},
render: createElement => createElement(App),
}).$mount('#app')
9. 插件
插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:
- 添加全局方法或者 property。如:
vue-custom-element
- 添加全局资源:指令/过滤器/过渡等。如
vue-touch
- 通过全局混入来添加一些组件选项。如
vue-router
- 添加 Vue 实例方法,通过把它们添加到
Vue.prototype
上实现。 - 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如
vue-router
9.1 开发插件
我们开发插件的时候,一般会将插件的功能放入一个JS文件中,例如我命名为 plugins.js
。并且这个js文件要暴露一个install函数出来,这个install函数是Vue自动帮你调用,无需自己调用,并且install函数第一个参数就是Vue构造器:
plugins.js
:
export default {
install(Vue,a,b,c){
console.log(Vue,a,b,c);
}
}
使用插件:
Vue.use(plugins,1,2,3)
可见第一个参数是Vue构造器,后面几个参数就是用户输入的参数了。至于为什么会有输出,是因为你使用Vue.use
的时候,Vue自动帮你执行了install函数。
export default {
install(Vue,a,b,c){
console.log(Vue,a,b,c);
//添加全局方法或 property
Vue.prototype.showName = (event)=>{
console.log('给Vue添加全局的方法');
console.log(event); //普通函数的第一个参数为事件对象
console.log(this) //install方法中的this就是install函数。
}
Vue.prototype.nihao = '你好'; //给Vue添加全局的property(属性)
//给Vue注入全局混入
Vue.mixin({
data() {
return {
x:100,
y:200
}
},
})
}
}
注意观察:
执行结果:
可见都是全局的。
9.2 使用插件
通过全局方法 Vue.use()
使用插件。它需要在你调用 new Vue()
启动应用之前完成:
Vue.use(插件名称)
new Vue({
// ...组件选项
})
注意:
Vue.use
会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件。
Vue.js
官方提供的一些插件 (例如 vue-router
) 在检测到 Vue
是可访问的全局变量时会自动调用 Vue.use()
。然而在像 CommonJS
这样的模块环境中,你应该始终显式地调用 Vue.use()
:
// 用 Browserify 或 webpack 提供的 CommonJS 模块环境时
var Vue = require('vue')
var VueRouter = require('vue-router')
// 不要忘了调用此方法
Vue.use(VueRouter)
10. scoped
scoped是使用在style标签上的,因为有这样一种场景,在实际开发中,有多个开发人员参与进开发工作中,员工A开发了一个组件,使用了类名.demo,并且配置了样式,员工B开发了一个组件,也使用了类名.demo,并且配置了样式,那么最后整个Vue在编译运行的时候,会把整个CSS文件整合,就会出现样式覆盖的情况,例如如下情况:
我在Student组件中设置样式为天蓝色:
<template>
<div>
<h2 class="demo">学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
</div>
</template>
<script>
export default {
name: 'Student',
data() {
return {
name: '念心卓',
sex: '男'
}
},
}
</script>
<style>
.demo {
background-color: skyblue;
}
</style>
我在School组件中设置样式为黄色:
<template>
<div>
<h2 class="demo">学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
</div>
</template>
<script>
export default {
name: 'School',
data() {
return {
name: 'B站大学',
address: 'www.bilibili.com',
}
},
}
</script>
<style >
.demo {
background-color: yellow;
}
</style>
在App组件中引入并且使用:
<template>
<div>
<School/>
<Student/>
</div>
</template>
<script>
import School from "@/components/School.vue";
import Student from "@/components/Student.vue";
export default {
name: 'App',
components: {
Student,
School
}
}
</script>
注意引入组件顺序(import),不是template标签使用组件顺序。
执行结果:
可见执行结果最后只有天蓝色了,因为我们在App引入组件的时候,后引入的Student组件,这就导致后面的样式将前面的样式覆盖了,现在我们调换一下顺序:
import Student from "@/components/Student.vue";
import School from "@/components/School.vue";
执行结果:
可见却是后引入的覆盖了先引入的。
那么怎样解决这种情况呢?这就要使用style标签的scoped属性了,当给style标签加上了这个属性之后,就表明这个组件的style中的样式就只为这个组件服务,不会出现在Vue编译运行时重新整合css样式文件而出现的覆盖情况。
现在我们给School、Student组件中的style标签加上scoped属性,再查看效果
<style scoped>
/*样式内容*/
</style>
执行结果:
可见这次就互不影响了。
总结:
- 作用:让样式在局部生效,防止冲突。
- 写法:
<style scoped>
11. 组件的自定义事件
在开始自定义组件的事件之前,我们先来回顾一下之前的props
配置,它是用于父组件向子组件传递数据的一个配置,那么如果我想要实现子组件向父组件传递数据呢,应该怎样做呢?
这里提供Vue中一种最基本的写法,用于子组件向父组件传递数据:
现在我有一个子组件Student,和一个父组件,App组件:
Student.vue
:
<template>
<div class="student">
<h2 >学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
</div>
</template>
<script>
export default {
name: 'Student',
data() {
return {
name: '念心卓',
sex: '男'
}
},
}
</script>
<style scoped>
.student {
background-color: skyblue;
padding: 5px;
margin-top: 10px;
}
</style>
App.vue:
<template>
<div class="app">
<School/>
<Student/>
</div>
</template>
<script>
import Student from "@/components/Student.vue";
import School from "@/components/School.vue";
export default {
name: 'App',
components: {
Student,
School
}
}
</script>
<style>
.app {
background-color: gray;
}
</style>
这里的School组件暂时先不贴出来。
现在我要求在Student组件中有一个按钮,点击按钮将学生名称送到App组件中:
Student.vue
:
<template>
<div class="student">
<h2 >学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
<button>点我输出学生姓名到App组件</button>
</div>
</template>
App.vue:
<template>
<div class="app">
<h1>当前输出的学生姓名为:{{studentName}}</h1>
<School/>
<Student/>
</div>
</template>
现在开始提供Vue中的基础写法:
在父组件中的data中定义一个对象属性来接收子组件传递过来的数据,例如上面App组件使用的是studentName,也就是子组件传递数据过来,存放到studentName属性中:
App.vue
中的js
部分export default { name: 'App', components: { Student, School }, data() { return { studentName:'' //首先要定义一个属性来接收数据 } } }
创建一个方法来将定义的属性进行赋值:
App.vue
中的js
部分export default { name: 'App', components: { Student, School }, data() { return { studentName:'' } }, methods: { showStudentName(value) { this.studentName = value; } } }
在
showStudentName
方法中,接收了一个参数,注意,这里的value就是子组件传递过来的值,如果子组件传递了多个值过来,这里就要写多个参数。将属性赋值的方法传递给子组件,以前props是怎样传递的,这里仍然怎样传递:
App.vue
中的template
部分<template> <div class="app"> <h1>当前输出的学生姓名为:{{ studentName }}</h1> <School/> <Student :showStudentName="showStudentName"/> </div> </template>
在子组件中首先要将父组件传递过来的数据接收到,还是使用props来接收:
Student.vue
中的js
部分export default { name: 'Student', data() { return { name: '念心卓', sex: '男' } }, props:['showStudentName'] }
然后在子组件中亲自调用父组件传递过来的方法:
Student.vue
完整代码,重点关注点击事件里面的执行逻辑<template> <div class="student"> <h2 >学生姓名:{{ name }}</h2> <h2>学生性别:{{ sex }}</h2> <button @click="outputStudentName">点我输出学生姓名到App组件</button> </div> </template> <script> export default { name: 'Student', data() { return { name: '念心卓', sex: '男' } }, props:['showStudentName'], methods:{ outputStudentName(){ this.showStudentName(this.name); } } } </script> <style scoped> .student { background-color: skyblue; padding: 5px; margin-top: 10px; } </style>
可见定义了一个点击事件,然后在点击事件中调用父组件传递过来的方法(执行),不过方法真正的执行过程仍然按照父组件中定义的这样来执行。
结果 :
可见传递成功。
补充:props适用于
- 父组件 ==> 子组件 通信
- 子组件 ==> 父组件 通信(要求父先给子一个函数)
可见,上述子组件像父组件传递数据,其实Vue中给的基本方法仍然是使用props,只不过父组件要传递的数据是函数。
11.1 绑定自定义事件
11.1.1 第一种写法
现在,还有一种方法,可以实现子组件向父组件传递数据,正如小节标题所示:组件的自定义事件。
组件的自定义事件其实也是一种组件间通信的方式,适用于:子组件 ===> 父组件
使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)。
例如还是使用本小节的上述代码进行改造,初始代码如下:
Student.vue
:
<template>
<div class="student">
<h2 >学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
<button @click="outputStudentName">点我输出学生姓名到App组件</button>
</div>
</template>
<script>
export default {
name: 'Student',
data() {
return {
name: '念心卓',
sex: '男'
}
},
methods:{
outputStudentName(){
//....这里面为空,暂时没有处理
}
}
}
</script>
<style scoped>
.student {
background-color: skyblue;
padding: 5px;
margin-top: 10px;
}
</style>
App.vue
:
<template>
<div class="app">
<h1>当前输出的学生姓名为:{{ studentName }}</h1>
<School/>
<Student/>
</div>
</template>
<script>
import Student from "@/components/Student.vue";
import School from "@/components/School.vue";
export default {
name: 'App',
components: {
Student,
School
}
}
</script>
<style>
.app {
background-color: gray;
}
</style>
现在的需求仍然是点击Student组件的按钮,将学生姓名传递给App这个父组件。
步骤:
声明接收到的数据,也就是
studentName
:App.vue
中的js
部分:data(){ return { studentName:'' } }
创建自定义事件,使用
v-on:xxx
,或者@xxx
,其中xxx
表示事件名称,后面的函数属于执行事件的回调函数App.vue
中的template
部分:<template> <div class="app"> <h1>当前输出的学生姓名为:{{ studentName }}</h1> <School/> <Student v-on:showStudentName="showName"/> </div> </template>
编写事件的回调函数,也就上述的
showName
:App.vue
中的js
部分:methods:{ showName(value){ this.studentName = value; } }
这里的value仍然是子组件调用事件的时候,传递过来的数据。
使用
$emit
来触发当前组件实例上的事件:Student.vue
中的js
部分:methods:{ outputStudentName(){ this.$emit('showStudentName',this.name) } }
$emit
有两个参数,第一参数是事件的名称,也就是你在父组件中使用的v-on:xxx
获取@xxx
,第二参数是需要传递的值,也就是你触发了这个事件,你要传递给该事件名对应的回函数的值。
结果仍然是传递成功的。
注意,这里解释几点:
- 在父组件中,给子组件绑定了自定义事件,例如:
<Student v-on:showStudentName="showName"/>
,这个showStudentName
事件就出现在了组件实例对象身上。this.$emit()
方法用于触发一个组件实例上的事件,这就能够理解了为啥要在具体组件中来调用这个$emit
方法了。this.$emit()
的主要用途是:
- 在子组件中触发事件
- 通知父组件某个事件发生
- 允许父组件在事件中访问传递的参数,这是Vue组件之间通信的一种方式,可以实现父组件对子组件事件作出响应
11.1.2 第二种写法
第一种写法是直接使用v-on:xxx或@xxx来绑定事件,不过有时候我们也会用第二种写法,他是基于ref
属性来完成的,差别并不是很大。
App.vue
部分的template
部分:
<template>
<div class="app">
<h1>当前输出的学生姓名为:{{ studentName }}</h1>
<School/>
<Student ref="showStudentName"/>
</div>
</template>
在App组件的mounted的生命周期函数中获取到Student组件实例,并且使用$on
来监听event事件:
mounted(){
this.$refs.showStudentName.$on('showName',this.displayStudentName)
}
$on方法有两个参数:
事件的名称:
注意这里的名称要和子组件中使用
$emit
中的事件名称对应上执行的回回调函数:
这里有一个小坑,最好是向我上面那样写,自己先将方法提供好,之后在$on中直接调用即可:
methods:{ displayStudentName(value){ this.studentName = value } },
你可能会想到这样写:
mounted(){ this.$refs.showStudentName.$on('showName',function (value){ console.log('###',this) this.studentName = value }) }
注意,这样写之后,你的this就不再是当前的App组件实例对象了,而是Student这个子组件的实例对象,因为你是将这个自定义事件绑定在了Student子组件,这里
$on
来监听的事件,其实也是监听的子组件的事件,所以,里面的普通函数回调中的this就是子组件,如果你非要这样写,那么你就必须使用箭头函数,因为箭头还是中的this是外层的对象的this。所以,我们一般还是不会在里面直接写具体的回调函数,而是用现有的函数。
在子组件Student中,依然使用$emit
来触发自定义事件:
methods:{
outputStudentName(){
this.$emit('showName',this.name)
}
}
千万千万要注意,这里的
$emit
中的事件名称必须要和$on
中的事件名称对应起来,或者反过来说也是一样的。
11.2 解绑自定义事件
对于事件的解绑,同样也要在子组件中进行解绑,因为当初绑定事件的时候也是在子组件上绑定的。
解绑事件使用组件实例对象身上的$off方法
:
methods:{
outputStudentName(){
this.$emit('showName',this.name) //绑定事件
this.$off('showName') //解绑事件
}
}
对于只有一个事件的情况,直接写事件名称即可
对于有多个事件的情况,通过数组的形式进行解绑:
this.$off(['event1','event2',...])
对于想要解绑全部事件,直接使用this.$off()
,无需写参数。
11.3 总结
一种组件间通信的方式,适用于:子组件 ===> 父组件
使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)。
绑定自定义事件:
第一种方式,在父组件中:
<Demo @事件名称="test"/>
或<Demo v-on:事件名称="test"/>
第二种方式,在父组件中:
<Demo ref="demo"/> ...... mounted(){ this.$refs.xxx.$on('事件名称',this.test) }
若想让自定义事件只能触发一次,可以使用
once
修饰符,或$once
方法。
触发自定义事件:
this.$emit('事件名称',数据)
解绑自定义事件
this.$off('事件名称')
组件上也可以绑定原生DOM事件,需要使用
native
修饰符。注意:通过
this.$refs.xxx.$on('事件名称',回调)
绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,否则this指向会出问题!
12. 全局事件总线(GlobalEventBus)
全局事件总线也是一种组件间通信的方式,适用于任意组件间通信。上一小节我们讲解了子到父组件的通信,这一节的全局事件总线,既可以父子之间通信,还可以兄弟组件之间通信。
全局事件总线并不是一个新的API,而是以前讲解过的知识的整合实现。在实现全局事件总线之前,我们先来设想一下,有这么一种结构:
在上图中,展示各个组件之间的关系。其中App组件中,有组件A、B、D,B组件中又有C组件,要实现任意组件之间的通信,我们可能会想要借用一个媒介或者说是桥梁,也就是上图中的X,来帮助我们实现任意组件之间的通信。
X充当媒介要满足如下基本要求:
- 要求所有的组件都能够看到这个组件
- 要有
$on
、$emit
、$off
等事件相关的函数
因为我之前说了,全局事件总线只是我们学过的知识来整合而实现的。
那如何实现上诉两点基本要求呢?
实现要求一:你可能会想到将X放到组件实例对象上,但是你要注意,每次使用组件的时候,其实都是一个新的组件实例对象,难道你每次使用组件的时候都放一次吗?显然不可能,最好的办法就是将他放到Vue实例对象上面去,这样所有的组件都能够看到。
别忘了
Vue
和VueComponent
之间的内置关系了:Vue.prototype === VueComponent.prototype.__proto__
可见从组件实例对象出发,仍然能够找到Vue所拥有的属性和方法。
实现要求二:在实现要求二之前,我们先搞清楚$on
、$emit
、$off
到底是谁拥有的。
const vm = new Vue({
components:{
App
},
render: createElement => createElement(App),
}).$mount('#app')
console.log(vm)
console.log(vm.__proto__)
可见,将X放到Vue实例身上恰好巧妙的将要求一和二都实现了。
所以,要实现任意组件之间的通信,步骤:
安装全局事件总线
new Vue({ components:{ App }, render: createElement => createElement(App), beforeCreate() { Vue.prototype.$bus = this; //安装全局事件总线,$bus就是当前应用的vm } }).$mount('#app')
这里的关键代码就是整个
beforeCreate
生命周期钩子所做的事情。这里将X替换为了$bus
,其中$
是为了迎合Vue的开发习惯,因为Vue中所有的$
开头的方法都是提供给程序员使用的,bus是因为他有总线的意思。至于为什么要赋值为this,因为这里的this就是Vue实例对象。使用事件总线
接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身。
methods(){ demo(data){......} } ...... mounted() { this.$bus.$on('xxxx',this.demo) }
如果想直接在
$on
的第二个参数中写函数,则必须要是箭头函数,具体原因可以参考第11小节。提供数据:
this.$bus.$emit('xxxx',数据)
,其中xxxx为事件名称,写在发生数据方
最好在
beforeDestroy
钩子中,用$off
去解绑当前组件所用到的事件。
例如Student组件想要传递StudentName给School组件:
Student.vue
:
<template>
<div class="student">
<h2 >学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
<button @click="showStudentName">点我输出学生姓名到School组件</button>
</div>
</template>
<script>
export default {
name: 'Student',
data() {
return {
name: '念心卓',
sex: '男'
}
},
methods:{
showStudentName(){
this.$bus.$emit('showName',this.name); //使用$emit触发(绑定)showName事件
}
}
}
</script>
<style scoped>
.student {
background-color: skyblue;
padding: 5px;
margin-top: 10px;
}
</style>
给组件Student
绑定一个点击事件,事件的回调为showStudentName
,在这里面进行showName
事件的触发或绑定。
School.vue
:
<template>
<div class="school">
<h2 class="demo">学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
<h2>当前就读该学校的学生名称:{{ studentName }}</h2>
</div>
</template>
<script>
export default {
name: 'School',
data() {
return {
name: 'B站大学',
address: 'www.bilibili.com',
studentName: ''
}
},
mounted() {
this.$bus.$on('showName',(data)=>{
console.log('监听到showName事件触发了(调用了$emit方法),收到的值为:',data)
this.studentName = data;
})
},
beforeDestroy() {
this.$bus.off('showName');
}
}
</script>
<style scoped>
.school {
background-color: yellow;
padding: 5px;
margin-top: 10px;
}
</style>
在School
组件中,当组件挂载完毕的时候,执行showName
事件的监听($on
),并且监听的回调写在了接收数据方,也就是这里的School。当组件销毁之前,要解绑想要的事件,但是不能将所有的事件都解绑,只能解绑属于当前组件的事件。
执行结果:
13. 消息订阅与发布(pubsub)
这里的消息订阅与发布也是一种组件之间的通信技术,适用于任意组件通信,不过这里需要用到第三方库,一般我们在Vue中较少使用这个技术,因为全局事件总线就可以写了。
步骤:
安装
pubsub-js
npm i pubsub-js
在需要使用的地方引入
pubsub
import pubsub from 'pubsub-js'
接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身。
methods(){ demo(data){......} } ...... mounted() { this.pid = pubsub.subscribe('xxx',this.demo) //订阅消息 }
同理这里的回调如果想要this指向为组件实例对象,则要写为箭头函数
提供数据:
pubsub.publish('xxx',数据)
最好在beforeDestroy钩子中,用
PubSub.unsubscribe(pid)
去取消订阅。
例如将之前的全局事件总线的代码拿来改造:
Student.vue
:
<template>
<div class="student">
<h2>学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
<button @click="showStudentName">点我输出学生姓名到School组件</button>
</div>
</template>
<script>
import Pubsub from "pubsub-js";
export default {
name: 'Student',
data() {
return {
name: '念心卓',
sex: '男'
}
},
methods: {
showStudentName() {
//this.$bus.$emit('showName',this.name); //使用$emit触发(绑定)showName事件
Pubsub.publish('showName', this.name); //使用订阅发布,发送数据方为发布者,使用publish函数,参数1为事件名,参数2为传递的值
}
}
}
</script>
<style scoped>
.student {
background-color: skyblue;
padding: 5px;
margin-top: 10px;
}
</style>
School.vue
:
<template>
<div class="school">
<h2 class="demo">学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
<h2>当前就读该学校的学生名称:{{ studentName }}</h2>
</div>
</template>
<script>
import Pubsub from 'pubsub-js'
export default {
name: 'School',
data() {
return {
name: 'B站大学',
address: 'www.bilibili.com',
studentName: ''
}
},
mounted() {
/*this.$bus.$on('showName',(data)=>{
console.log('监听到showName事件触发了(调用了$emit方法),收到的值为:',data);
this.studentName = data;
})*/
//这里的回调也可以写在methods中,然后使用this.xxxx调用即可
Pubsub.subscribe('showName', (eventName,data) => { //注意,subscribe有两个参数,第一个参数为事件名,第二个参数为接收的值
console.log(eventName,data);
this.studentName = data;
});
},
beforeDestroy() {
// this.$bus.off('showName');
Pubsub.unsubscribe('showName'); //组件销毁之前,取消该组件订阅的事件
}
}
</script>
<style scoped>
.school {
background-color: yellow;
padding: 5px;
margin-top: 10px;
}
</style>
执行结果:
可见消息的订阅和发布完全可以不用,完全可以用全局事件总线来做,基本上没什么区别。
14. nextTick
- 语法:
this.$nextTick(回调函数)
- 作用:在下一次 DOM 更新结束后执行其指定的回调。
- 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行。
例如我下面的案例,我有两个组件Student
、School
,在Student
组件中,我点击按钮,将学生姓名传递给School组件,School组件展示的时候,如果学生姓名为空,就不展示,反之则展示。
Student.vue
:
<template>
<div class="student">
<h2>学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
<button @click="showStudentName">点我输出学生姓名到School组件</button>
</div>
</template>
<script>
export default {
name: 'Student',
data() {
return {
name: '念心卓',
sex: '男'
}
},
methods: {
showStudentName() {
this.$bus.$emit('showName',this.name); //使用$emit触发(绑定)showName事件
}
}
}
</script>
<style scoped>
.student {
background-color: skyblue;
padding: 5px;
margin-top: 10px;
}
</style>
School.vue
:
<template>
<div class="school">
<h2 class="demo">学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
<h2 v-if="studentName !== ''" ref="showH2">当前就读该学校的学生名称:{{ studentName }}</h2>
</div>
</template>
<script>
export default {
name: 'School',
data() {
return {
name: 'B站大学',
address: 'www.bilibili.com',
studentName: ''
}
},
mounted() {
this.$bus.$on('showName',(data)=>{
console.log('监听到showName事件触发了(调用了$emit方法),收到的值为:',data);
this.studentName = data;
console.log( this.$refs.showH2); //获取到h2这个DOM对象
})
},
beforeDestroy() {
this.$bus.off('showName');
}
}
</script>
<style scoped>
.school {
background-color: yellow;
padding: 5px;
margin-top: 10px;
}
</style>
执行结果:
可见并没有获取到,这是为什么呢?难道是mount中第二个输出语句没有被执行吗?这是不可能的,都打印出来了undefined
的了,那这是为什么呢?
其实这里就是Vue加载顺序的问题:当Vue检测到数据改变之后,会去重新解析模板,就是这里重新去解析模板出了问题,当执行到this.studentName = data;
语句的时候,确实数据改变了,但是此时Vue并没有着急去重新解析模板,而是将console.log( this.$refs.showH2);
语句执行了之后再去解析的模板,由于你执行这句代码的时候,模板还没重新解析,又因为你是用的v-if
,即条件不满足的时候DOM节点都没有,当然输出undefined
。
那么就想要解决这个问题,就要使用新的API:$nextTick
;我们希望当h2渲染出来的时候,再去执行 console.log( this.$refs.showH2);
;这样就能够打印出数据了。而**$nextTick的作用就是在下一次 DOM 更新结束后执行其指定的回调。**
修改School.vue
的代码:
mounted() {
this.$bus.$on('showName',(data)=>{
console.log('监听到showName事件触发了(调用了$emit方法),收到的值为:',data);
this.studentName = data;
this.$nextTick(function () {
console.log( this.$refs.showH2);
})
});
}
执行结果:
可见成功。