Vue2脚手架


Vue2 - 脚手架

1. 初始化脚手架

参考地址:https://cli.vuejs.org/zh/

  1. 首先你得要有Node.js的环境,有了环境之后,你就可以使用npm命令了。

  2. 给npm下载地址配置为淘宝镜像:

    npm config set registry https://registry.npm.taobao.org
    
  3. 之后全局安装Vue-CLI:

    npm install -g @vue/cli
    
  4. 查看是否可以使用vue命令:

  5. 使用Vue-CLI来创建项目:

    vue create projectName
    

    注意:这里执行CMD命令的时候,必须要在你想要的目录下进行。

  6. 之后就跟着选择,等待安装即可:

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。

详细说明参考官网:https://cli.vuejs.org/zh/guide/mode-and-env.html#%E5%9C%A8%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%BE%A7%E4%BB%A3%E7%A0%81%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F

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:

  1. vue.jsvue.runtime.xxx.js的区别:
    (1).vue.js是完整版的Vue,包含:核心功能+模板解析器。
    (2).vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。

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

这里的解决办法:

  1. 使用带有模板编译器的Vue.js文件
  2. 使用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来解决

参考文档:https://cli.vuejs.org/zh/config/

再次启动成功。

补充:

这里还有很多配置,可以自行查看

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>

可见,如果作用到了组件上,那么你获取到的就是整个组件实例对象。


总结:

  1. 被用来给元素或子组件注册引用信息(id的替代者
  2. 应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象VueComponent
  3. 使用方式:
    1. 打标识:<h1 ref="xxx">.....</h1><School ref="xxx"></School>
    2. 获取:```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

可见现在目的达到。


这里还有几个注意点

  1. 在组件中传递数据,传递过来的都是字符串

    例如上面代码,对于age数据:

    可见都是字符串,那要如果获取到Number类型的数据呢?

    使用以前的v-bind即可:<Student name="念心卓" sex="男" :age="18"/>;因为v-bind会解析标签属性中的JavaScript表达式,会忽略外侧的引号。

    可见此刻是Number类型的。

  2. 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的地方改回去了。

  3. 不建议直接更改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,所以你的插值语法处也要用这个。

  4. 当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(混入步骤):

  1. 编写将要混入的代码抽出来,存入js文件中

    例如我将上面的输出名称的方法抽取出来,封装到mixin.js文件中

    export const hunru = {
        methods: {
            showName(){
                console.log(this.name)
            }
        },
    }
    

    这里我采用分别暴露的方式,因为可能会有多个对象要被暴露出去

    重要点:你之前的代码怎么写的,这里就怎么写,只不过需要暴露出去,而且里面的this,如果在组件中引用这个js,那么this就是这个组件实例对象,如果是在vm中引入这个js,那么这个this就是Vue实例vm

    目录结构:

  2. 将之前的写的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 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:

  1. 添加全局方法或者 property。如:vue-custom-element
  2. 添加全局资源:指令/过滤器/过渡等。如 vue-touch
  3. 通过全局混入来添加一些组件选项。如 vue-router
  4. 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  5. 一个库,提供自己的 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>

执行结果:

可见这次就互不影响了。

总结:

  1. 作用:让样式在局部生效,防止冲突
  2. 写法:<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中的基础写法

  1. 父组件中的data中定义一个对象属性来接收子组件传递过来的数据,例如上面App组件使用的是studentName,也就是子组件传递数据过来,存放到studentName属性中:

    App.vue中的js部分

    export default {
      name: 'App',
      components: {
        Student,
        School
      },
      data() {
        return {
          studentName:'' //首先要定义一个属性来接收数据
        }
      }
    }
    
  2. 创建一个方法来将定义的属性进行赋值

    App.vue中的js部分

    export default {
      name: 'App',
      components: {
        Student,
        School
      },
      data() {
        return {
          studentName:''
        }
      },
      methods: {
        showStudentName(value) { 
          this.studentName = value;
        }
      }
    }
    

    showStudentName方法中,接收了一个参数,注意,这里的value就是子组件传递过来的值,如果子组件传递了多个值过来,这里就要写多个参数

  3. 将属性赋值的方法传递给子组件,以前props是怎样传递的,这里仍然怎样传递

    App.vue中的template部分

    <template>
      <div class="app">
        <h1>当前输出的学生姓名为:{{ studentName }}</h1>
        <School/>
        <Student :showStudentName="showStudentName"/>
      </div>
    </template>
    
  4. 在子组件中首先要将父组件传递过来的数据接收到,还是使用props来接收

    Student.vue中的js部分

    export default {
      name: 'Student',
      data() {
        return {
          name: '念心卓',
          sex: '男'
        }
      },
      props:['showStudentName']
    }
    
  5. 然后在子组件中亲自调用父组件传递过来的方法:

    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适用于

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

可见,上述子组件像父组件传递数据,其实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这个父组件。

步骤:

  1. 声明接收到的数据,也就是studentName

    App.vue中的js部分:

    data(){
        return {
          studentName:''
        }
    }
    
  2. 创建自定义事件,使用v-on:xxx,或者@xxx,其中xxx表示事件名称,后面的函数属于执行事件的回调函数

    App.vue中的template部分:

    <template>
      <div class="app">
        <h1>当前输出的学生姓名为:{{ studentName }}</h1>
        <School/>
        <Student v-on:showStudentName="showName"/>
      </div>
    </template>
    
  3. 编写事件的回调函数,也就上述的showName

    App.vue中的js部分:

    methods:{
        showName(value){
          this.studentName = value;
        }
    }
    

    这里的value仍然是子组件调用事件的时候,传递过来的数据。

  4. 使用$emit来触发当前组件实例上的事件

    Student.vue中的js部分:

    methods:{
        outputStudentName(){
          this.$emit('showStudentName',this.name)
        }
    }
    

    $emit有两个参数,第一参数是事件的名称,也就是你在父组件中使用的v-on:xxx获取@xxx第二参数是需要传递的值,也就是你触发了这个事件,你要传递给该事件名对应的回函数的值。

结果仍然是传递成功的。

注意,这里解释几点:

  1. 在父组件中,给子组件绑定了自定义事件,例如:<Student v-on:showStudentName="showName"/>,这个showStudentName事件就出现在了组件实例对象身上。
  2. this.$emit()方法用于触发一个组件实例上的事件,这就能够理解了为啥要在具体组件中来调用这个$emit方法了。
  3. 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方法有两个参数:

  1. 事件的名称:

    注意这里的名称要和子组件中使用$emit中的事件名称对应上

  2. 执行的回回调函数

    这里有一个小坑,最好是向我上面那样写,自己先将方法提供好,之后在$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 总结

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

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

  3. 绑定自定义事件:

    1. 第一种方式,在父组件中:<Demo @事件名称="test"/><Demo v-on:事件名称="test"/>

    2. 第二种方式,在父组件中:

      <Demo ref="demo"/>
      ......
      mounted(){
         this.$refs.xxx.$on('事件名称',this.test)
      }
      
    3. 若想让自定义事件只能触发一次,可以使用once修饰符,或$once方法。

  4. 触发自定义事件:this.$emit('事件名称',数据)

  5. 解绑自定义事件this.$off('事件名称')

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

  7. 注意:通过this.$refs.xxx.$on('事件名称',回调)绑定自定义事件时,回调要么配置在methods中要么用箭头函数,否则this指向会出问题!

12. 全局事件总线(GlobalEventBus)

全局事件总线也是一种组件间通信的方式,适用于任意组件间通信。上一小节我们讲解了子到父组件的通信,这一节的全局事件总线,既可以父子之间通信,还可以兄弟组件之间通信。

全局事件总线并不是一个新的API,而是以前讲解过的知识的整合实现。在实现全局事件总线之前,我们先来设想一下,有这么一种结构:

在上图中,展示各个组件之间的关系。其中App组件中,有组件A、B、D,B组件中又有C组件,要实现任意组件之间的通信,我们可能会想要借用一个媒介或者说是桥梁,也就是上图中的X,来帮助我们实现任意组件之间的通信。

X充当媒介要满足如下基本要求

  1. 要求所有的组件都能够看到这个组件
  2. 要有$on$emit$off等事件相关的函数

因为我之前说了,全局事件总线只是我们学过的知识来整合而实现的。

那如何实现上诉两点基本要求呢?

实现要求一:你可能会想到将X放到组件实例对象上,但是你要注意,每次使用组件的时候,其实都是一个新的组件实例对象,难道你每次使用组件的时候都放一次吗?显然不可能,最好的办法就是将他放到Vue实例对象上面去,这样所有的组件都能够看到。

别忘了VueVueComponent之间的内置关系了: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实例身上恰好巧妙的将要求一和二都实现了

所以,要实现任意组件之间的通信,步骤:

  1. 安装全局事件总线

    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实例对象。

  2. 使用事件总线

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

      methods(){
        demo(data){......}
      }
      ......
      mounted() {
        this.$bus.$on('xxxx',this.demo)
      }
      

      如果想直接在$on的第二个参数中写函数,则必须要是箭头函数,具体原因可以参考第11小节。

    • 提供数据:this.$bus.$emit('xxxx',数据),其中xxxx为事件名称,写在发生数据方

  3. 最好在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中较少使用这个技术,因为全局事件总线就可以写了

步骤:

  1. 安装pubsub-js

    npm i pubsub-js
    
  2. 在需要使用的地方引入pubsub

    import pubsub from 'pubsub-js'
    
  3. 接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身

    methods(){
      demo(data){......}
    }
    ......
    mounted() {
      this.pid = pubsub.subscribe('xxx',this.demo) //订阅消息
    }
    

    同理这里的回调如果想要this指向为组件实例对象,则要写为箭头函数

  4. 提供数据:pubsub.publish('xxx',数据)

  5. 最好在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

  1. 语法:this.$nextTick(回调函数)
  2. 作用:在下一次 DOM 更新结束后执行其指定的回调
  3. 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行

例如我下面的案例,我有两个组件StudentSchool,在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);
      })
    });
}

执行结果:

可见成功。


文章作者: 念心卓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 念心卓 !
  目录