【Vue】系列二 - 组件化和增强配置


一、组件化

组件:实现应用中局部功能代码和资源的集合。

1.1. 组件定义

Vue中使用组件的三大步骤:

  1. 定义组件(创建组件)
    • 使用Vue.extend(options)创建,其中optionsnew Vue(options)里面传入的options几乎一样,但也有区别
      • el不要写。因为最终所有的组件都要经过同一个vm的管理,由vm中的el决定服务哪个容器。
      • data必须写成函数。因为对象在内存中只存在一份,为了避免组件被复用时数据存在引用关系,只能写成函数(每次都返回新的对象)。
    • 使用template可以配置组件的HTML结构。
    • 简写:const school = Vue.extend(options) 可简写为 const school = options
  2. 注册组件
    • 局部注册:new Vue传入components选项
    • 全局注册:Vue.component('组件名', 组件)
  3. 使用组件(写组件标签)
    • 例:<school></school>
    • 不使用脚手架时,<school/>写法会导致后续组件不能渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<body>
<div id="root">
<h2>原始数据:<span v-text="n"></span></h2>
<button @click="n++">n++</button>

<!-- 使用组件 -->
<school></school>
<student></student>
</div>

<script>
// 1. 定义组件
const school = Vue.extend({
template: `
<div>
<h2>学校名称:{{name}}</h2>
学生数量:<input type="number" v-model.number="number" />
</div>
`,
data() {
return {
name: "北大",
number: "",
};
},
});

const student = Vue.extend({
template: `
<div>
<h2>学生姓名:{{name}}</h2>
学生班级:<input type="text" v-model="classes" />
</div>
`,
data() {
return {
name: "张三",
classes: "",
};
},
});

// 全局注册
// Vue.component('school', school)
// Vue.component('student', student)

let vm = new Vue({
el: "#root",
data: {
n: 1,
},
// 2. 注册组件(局部注册)
components: {
school,
student
}
});
</script>
</body>

1.2. 组件名

  • 一个单词
    • 首字母小写:school
    • 首字母大写:School
  • 多个单词
    • kebab-case:my-school
    • 驼峰:MySchool(需要Vue脚手架支持)

组件名尽可能回避HTML中已有的元素名称,例如:h2H2都不行。

可以使用name配置项指定组件在开发者工具中显示的名字。例:const school = Vue.extend({name:'Custom-School'})。如果没有配置name,默认会读取注册组件的名字。

1.3. VueComponent

  1. school组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的。
  2. 我们只需要写<school/><school></school>,Vue解析时会帮我们创建school组件的实例对象,即Vue帮我们执行了new VueComponent(options)
  3. 特别注意:每次调用Vue.extend返回的都是一个全新的VueComponent
  4. 关于this指向问题:
    • 组件配置中:data函数、methods中的函数、watch中的函数、computed中的函数,它们的this都是VueComponent实例对象
    • new Vue()配置中:data函数、methods中的函数、watch中的函数、computed中的函数,它们的this都是Vue实例对象
  5. VueComponent的实例对象,简称vc,也可称为组件实例对象
  6. Vue的实例对象:简称vm

Vue与VueComponent的关系:

为什么会有这个关系(VueComponent.prototype.__proto__ === Vue.prototype)?目的是让组件实例对象(vc)可以访问到Vue原型上的属性和方法(Vue框架代码的复用)。

二、单文件组件

2.1. 格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 1. 模板 -->
<template>
<div>
<!-- 必须有根标签 -->
</div>
</template>

<!-- 2. 脚本 -->
<script>
// 导出组件
export default {
name: 'App' // 组件名称
}
</script>

<!-- 3. 样式 -->
<style scoped>

</style>

2.2. 脚手架

  1. 全局安装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 安装
    npm install -g @vue/cli
    # OR
    yarn global add @vue/cli

    # 升级
    npm update -g @vue/cli
    # OR
    yarn global upgrade --latest @vue/cli
  2. 切换到要创建项目的目录,使用CLI命令创建项目

    1
    vue create xxx
  3. 启动项目

    1
    2
    3
    npm run serve
    # OR
    yarn serve

2.3. render

vue.jsvue.runtime.xxx.js的区别:

  1. vue.js是完整版的Vue,包含核心功能和模板解析器
  2. vue.runtime.xxx.js是运行版的Vue,只包含核心功能,没有模板解析器。

主要目的是为了减少包体积,而且项目上线后是不需要模板解析器的,因为最终都是HTML+CSS+JS。

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

三、增强

3.1. ref

  1. 被用来给元素或子组件注册引用信息(id的替代者)

  2. 应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(vc)

使用方式:

1
2
3
4
5
<!-- 打标识 -->
<h1 ref="xxx">...</h1> 或 <School ref="xxx"></School>

<!-- 获取 -->
this.$refs.xxx

3.2. props

组件传值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!-- 传值,注意如果是非字符串类型(例如:age),需要使用v-bind形式 -->
<Student name="张三" sex="男" :age="10"></Student>

<!-- 使用 -->
<script>
export default {
name: 'Student',
// props: ['name', 'sex', 'age'] // 1. 简单形式
/* 2. 限制类型
props: {
name: String,
age: Number,
sex: String
}
*/
// 3. 完整版本
props: {
name: {
type: String,
required: true // 必传
},
age: {
type: Number,
default: 99 // 默认值
},
sex: {
type: String,
required: true
}
}
}
</script>
  1. defaultrequired是互斥的,不能同时存在。
  2. 外部传入的props在组件内部尽量不要直接修改(直接修改Vue会发出警告),如果需要修改,可以在data中定义一个新的属性,属性值是props中的属性值,最终通过修改data中的属性达到目的,如下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
export default {
name: 'Student',
data() {
return {
myAge: this.age // props会被优先执行,并且props中的属性能够在当前组件实例对象中访问
}
},
methods: {
updateAge(){
this.myAge++
}
},
props: {
age: {
type: Number,
default: 99
}
}
}
</script>

补充:props不仅局限于String和Number,还可以传函数。

3.3. mixin

混入可以把多个组件共用的配置提取成一个混入对象(不影响原有逻辑)。

  • 如果组件中的data数据和mixin中的data有相同的属性,优先使用组件中的data数据
  • 组件中的生命周期函数和mixin中的生命周期都是独立的,互相没有影响。mixin中的生命周期函数优先执行

混入方式:局部混入和全局混入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!-- Student.vue -->
<template>
<div>
<h1 @click='showInfo'>测试混入</h1>
</div>
</template>

<script>
// 1. 引入
import {testRun} from '../mixin'

export default {
name: 'Student',
data() {
return {
name: ''
}
},
// 2. 使用(局部引入)
mixins: [testRun]
}
</script>

<!-- mixin.js -->
export const testRun = {
data() {
return {
msg: '混入msg',
x: 0,
y: 0
}
},
methods: {
showInfo() {
alert(this.msg)
}
},
mounted: {

}
}

全局混入:Vue.mixin(testRun)。如果有更多的混入项,多次进行配置即可。

3.4. plugins

插件的功能是为了增强Vue。本质上是一个包含install方法的对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据(可选)。

定义插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default {
install(Vue, options) {
// 1. 添加全局过滤器
Vue.filter(...)

// 2. 添加全局指令
Vuedirective(...)

// 3. 配置全局混入
Vue.mixin(...)

// 4. 添加实例方法
Vue.prototype.$myProperty = xxx
Vue.prototype.$myMethod = function(){...}
}
}

使用插件(必须在实例化Vue之前使用):

1
Vue.use(xxx)

3.5. scoped

Vue中所有编写的组件style最终都会合并到一个css文件中,所以style很大可能会有冲突覆盖问题。

只需要给style标签加上scoped关键词就可以让每个组件的样式互相隔离,只在组件内部生效。

1
2
3
<style scoped>

</style>

本质上是Vue给组件最外层标签加了一个随机属性(<div data-v-33gfsgb class="demo"></div>),并用属性选择器.demo[data-v-33gfsgb]{background-color: red}为其添加样式。

如果指定style的langless,需要额外添加less-loader,但是需要注意webpack和less-loader的版本兼容问题,例如:less-loader的7.x对应webpack的4.x,安装时需要先查看脚手架中webpack的版本,然后再去安装指定版本的less-loadernpm i less-loader@7

3.6. 自定义事件

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

子给父传递数据:

  1. 父组件给子组件传递函数类型的props实现(可能会套娃)
  2. 父组件给子组件绑定一个自定义事件实现(@事件名ref

3.6.1. emit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- App.vue -->
<Student></Student>

<script>
methods: {
handleStudent(value) {
console.log(value) // 输出:你好
}
}
</script>

<!-- Student.vue -->
<script>
methods: {
showMsg() {
// 发射stuevent事件
this.$emit('stuevent', '你好')
}
}
</script>

3.6.2. on

绑定原则:谁绑定(on)事件,谁触发(emit)事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- App.vue -->
<Student ref="student"></Student>

<script>
methods: {
handleStudent() {
console.log(value) // 输出:你好
}
},
mounted: {
// 监听stuevent事件
this.$refs.student.$on('stuevent', this.handleStudent)
}
</script>

<!-- Student.vue -->
<script>
methods: {
showMsg() {
// 触发stuevent事件
this.$emit('stuevent', '你好')
}
}
</script>

如果通过on监听事件的方式,并且只需要触发一次事件时,需要把$on替换为$once

如果是通过@事件名方式,自定义事件也可以使用事件修饰符(如:@stuevent.once="xxx")。

3.6.3. off

使用$off可以把解绑自定义事件。

解绑原则:谁触发(emit)事件,谁解绑(off)事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- Student.vue -->
<script>
methods: {
showMsg() {
// 触发stuevent事件
this.$emit('stuevent', '你好')
},
unbind() {
// 解绑一个自定义事件
this.$off('stuevent')
// 解绑多个自定义事件
this.$off(['stuevent', 'other'])
// 解绑所有自定义事件
this.$off()
}
}
</script>

注意:Vue销毁后,销毁的事件就是类似上面的自定义事件(包含Vue内置事件,但原生事件不受影响)。

组件上也可以绑定原生DOM事件,如<Student @click="xxx">这样使用,其实绑定的是自定义click事件,如果需要原生事件响应,需要使用native修饰符,<Student @click.native="xxx">

通过this.$refs.xxx.$on('stuevent', 回调)绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,普通函数this指向会出问题(绑定的是哪个组件,this指向的就是这个组件)。

3.7. 全局事件总线

自定义事件适用于子组件给父组件传递数据,而全局事件总线适用于任意组件间通信

  1. 安装全局事件总线

    1
    2
    3
    4
    5
    6
    7
    8
    <script>
    new Vue({
    //...
    beforeCreate() {
    Vue.prototype.$bus = this // $bus就是当前应用的vm
    }
    })
    </script>

    $bus绑定到Vue原型对象上,VueComponent实例对象也可以访问。

  2. 使用事件总线

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <script>
    methods() {
    demo(data){
    ......
    }
    }
    mounted() {
    this.$bus.$on('xxx', this.demo)
    }
    </script>

    提供数据:this.$bus.$emit('xxx', 数据)

  3. 解绑事件

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

    1
    2
    3
    beforeDestory() {
    this.$bus.$off('xxx')
    }

事件总线是程序员总结的经验成果,并不是Vue官方的API。

3.8. 消息订阅与发布

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

  1. 安装第三方库:npm i pubsub.js
  2. 引入:import pubsub from 'pubsub.js'
  3. 订阅(接收数据):this.pubId = pubsub.subscribe('hello', function(msgName, data) {})
  4. 发布(提供数据):pubsub.publish('hello', '你好')
  5. 取消订阅:pubsub.unsubscribe(pubId)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
import pubsub from 'pubsub.js'
export default {
mounted() {
// 订阅
// 注意:不要使用普通函数,否则this指向会有问题(建议使用箭头函数或回调函数卸载methods中)
this.pubId = pubsub.subscribe('hello', (msgName, data) => {
// msgName: 订阅消息名
// data: 订阅参数
// return 返回订阅id
})
},
beforeDestroy() {
// 取消订阅(传入订阅id)
pubsub.unsubscribe(this.pubId)
}
}
</script>

建议使用全局事件总线,因为是官方提供的套路。并且消息订阅与发布在使用上和全局事件总线也类似,开发者工具看不到pubsub的订阅发布事件,因此没有更多必要使用第三方库。

3.9. nextTick

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

使用场景:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div>
<input ref="titleRef"/>
</div>
</template>

<script>
methods() {
handleEdit() {
// 如果直接获取focus,input获取焦点就会失败,因为默认情况下Vue会把handleEdit里面的所有代码执行完毕后再更新DOM,而此时执行focus,会找不到input。
// this.$refs.titleRef.focus()

// 使用定时器(不推荐)
/*
setTimeout(() => {
this.$refs.titleRef.focus()
}, 200)
*/

this.$nextTick(function() {
this.$refs.titleRef.focus()
})
}
}
</script>

3.10. 动画

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

  1. 准备好样式

    • 元素进入的样式
      • v-enter:进入的起点
      • v-enter-active:进入过程中
      • v-enter-to:进入的终点
    • 元素离开的样式
      • v-leave:离开的起点
      • v-leave-active:离开过程中
      • v-leave-to:离开的终点
  2. 使用<transition>包裹要过渡的元素,并配置name属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!-- appear为true时可以让页面进入时直接执行动画 -->
    <transition name="hello" appear>
    <h1 v-show="isShow">你好</h1>
    </transition>

    <style>
    .hello-enter-active {
    animation: testAnimate 0.5s linear;
    }

    .hello-leave-active {
    animation: testAnimate 0.5s linear reverse;
    }

    @keyframes testAnimate {
    from {
    transform: translateX(100%);
    }
    to {
    transform: translateX(0px);
    }
    }
    </style>

    若多个元素需要过渡,则需要使用<transition-group>,且每个元素都要指定key值。

使用第三方库:

  • 安装:npm install animate.css | yarn add animate.css
  • 引入:import 'animate.css'
  • 使用:<transition name="animate__animated animate__bounce" enter-active-class="" leave-active-class="">,具体可以参考官方文档

3.11. 插槽

3.11.1. 默认插槽

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 父组件中使用子组件 -->
<Category>
<div>html结构</div>
</Category>

<!-- 子组件 -->
<template>
<div>
<!-- 定义插槽 -->
<slot>插槽默认内容</slot>
</div>
</template>

3.11.2. 具名插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 父组件中使用子组件 -->
<Category>
<div slot="center">
<div>html结构</div>
</div>

<!-- 新版写法:v-slot(仅限和template进行搭配使用) -->
<template v-slot:footer>
<div>html结构</div>
</template>
</Category>

<!-- 子组件 -->
<template>
<div>
<!-- 定义插槽 -->
<slot name="center">插槽默认内容</slot>
<slot name="footer">插槽默认内容</slot>
</div>
</template>

3.11.3. 作用域插槽

场景:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!-- 父组件中使用子组件 -->
<Category>
<template scope="scopeData">
<ul>
<li v-for="g in scopeData.games" :key="g">{{ g }}</li>
</ul>
</template>
</Category>

<!-- 新版写法 -->
<Category>
<template slot-scope="scopeData">
<ul>
<li v-for="g in scopeData.games" :key="g">{{ g }}</li>
</ul>
</template>
</Category>

<!-- 子组件 -->
<template>
<div>
<slot :games="games"></slot>
</div>
</template>

<script>
export default {
name: 'Category',
props: ['title'],
data() {
return {
games: ['红警', '英雄联盟', '穿越火线', '梦幻西游']
}
}
}
</script>

四、axios

网络请求框架:

  • xhr(XMLHttpRequest):太麻烦
  • jQuery:封装了xhr请求,但是体量太大(核心代码是为了操作DOM,只有少部分代码是网络请求)
  • fetch:和xhr平级的,比较难用(返回的数据有两层Promise,IE浏览器不能使用)
  • axios:轻量,并且Vue官方推荐

4.1. 跨域

jsonp解决跨域:需要前后端配合,并且只能解决GET请求的跨域。

Vue官方使用代理解决跨域问题:

方法一:在vue.config.js中进行如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
devServer: { // 开启代理服务,目标服务器的端口是9000
proxy: 'http://localhost:9000'
}
}

// axios
axios.get('http://localhost:8080/students').then(
response => {
console.log('响应数据:', response.data)
},
error => {
console.log('请求失败:', error.message)
}
)

本地服务(http://localhost:8080)默认情况下会在项目根目录下查找文件,所以使用axios请求时,请求路径不能和根目录下的文件同名,否则会优先请求public目录下的同名文件。而且上面的配置方式只能配置一个代理,为了解决这几个问题,VueCLI提供了下面的代理配置方式。

方法二:在vue.config.js中配置代理规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
devServer: {
proxy: {
'^/api': { // 匹配所有以/api开头的请求路径,如:http://localhost:8080/api/students
pathRewrite: {
'^/api': '' // 把请求的前缀/api移除,否则发送到服务器的请求地址就会包含/api
},
target: 'http://localhost:9000', // 目标服务器地址
ws: true, // 是否支持websocket(默认:true)
changeOrigin: true // 是否修改请求主机(默认:true)(代理服务器会告诉目标服务器自己来自哪个主机和端口号。如果为true,服务器收到的请求头中的host为localhost:9000;如果为false,服务器收到的请求头中的host为localhost:8080)
},
'^/foo': {
target: '<other_url>'
}
}
}
}

上面方法二的优点是可以配置多个代理,且可以灵活的控制请求是否走代理。缺点是配置略微繁琐,请求资源时必须加前缀。

4.2. vue-resource

Vue的插件库,在vue1.x使用的很广泛,使用流程和axios基本类似,只需要安装vue-resource,然后引用插件,最终this.$http.get().then()使用就可以了。

目前官方已经不再维护(交给其他团队了)。