本章概要
- 单文件组件
- 组件通信的其它方式
- 访问根实例
- 访问父组件实例
- 访问子组件实例或子元素
- provide 和 inject
10.10 单文件组件
在很多 Vue 项目中,全局组件使用 app.component() 方法定义,然后使用 app.mount(‘#app’) 在页面内绑定一个容器元素。
这种方式在很多小规模的项目中运作的很好,在这些项目里 JavaScript 只是被用来加强特定的视图。然而,在更复杂的项目中,或者前端完全由JavaScript驱动时,以下缺点将变得非常明显:
- 全局定义强制要求每个组件的命名不能重复
- 字符串模板缺乏语法高亮显示,在 HTML 有多行的时候,需要用到反斜杠,或者 ES6 中的反引号“`”,而后者依赖于支持 ES6 的浏览器
- 没有 CSS 的支持意味着当 HTML 和 JavaScript 被模块化为组件时,CSS 明显被遗漏了
- 没有构建步骤,这限制为只能使用 HTML 和 ES5 JavaScript,而不能使用预处理器,如果 Pug一千的(Jade)和 Babel
在 Vue.js 中,可以使用单文件组件解决上述所有问题。在一个文件扩展名为 .vue 的文件中编写组件,可以将组件模板代码以 HTML 的方式书写,同时 JavaScript 与 CSS 代码也在同一个文件中编写。
例如:
<template>
<div>
<ul class="item">
<li class="username">用户名:{{ post.user.username }},留言时间:{{ gstTime }}</li>
<li class="title">主题:{{ post.title }},</li>
<li>内容:{{ post.content }}</li>
</ul>
</div>
</template>
<style scoped>
.item {
border-top: solid 1px grey;
padding: 15px;
font-size: 14px;
color: grey;
line-height: 21px;
}
.username{
font-size: 16px;
font-weight: bold;
line-height: 24px;
color: #009a61;
}
.title {
font-size: 16px;
font-weight: bold;
line-height: 24px;
color: #009a61;
}
ul li {
list-style: none;
}
</style>
<script>
export default{
name:'postItem',
data() {
return {}
},
props:['post'],
computed:{
gstTime:function(){
let d = new Date(this.post.gstTime);
d.setHours(d.getHours - 8);
return d.toLocaleString();
}
}
}
</script>
在单文件组件中编写 CSS 样式规则时,可以添加一个 scoped 属性。该属性的作用是限定 CSS 样式只作用于当前组件元素,相当于是组件作用域的 CSS。
10.11 杂项
介绍组件开发中一些不常用但特殊需求下会用到的功能。
10.11.1 组件通信的其它方式
总结一下前面介绍的组件通信的 3 种方式:
- 父组件通过 prop 向子组件传递数据
- 子组件通过自定义事件向父组件发起通知或进行数据传递
- 子组件通过 slot 元素充当占位符,获取父组件分发的内容;也可以在子组件的 slot 元素上使用 v-bind 指令板顶一个插槽 prop ,向父组件提供数据
而此处将介绍组件通信的其它实现方式。
1. 访问根实例
在每一个根组件实例的子组件中,都可以通过 root 属性访问根实例。如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<parent></parent>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
data() {
return {
price: 188
}
},
computed: {
totalPrice() {
return this.price * 10;
}
},
methods: {
hello() {
return "葵花宝典";
}
}
})
app.component('parent', {
template: '<child></child>'
})
app.component('child', {
methods: {
accessRoot() {
console.log("单价:" + this.$root.price);
console.log("总价:" + this.$root.totalPrice);
console.log(this.$root.hello());
}
},
template: '<button @click="accessRoot">访问根实例</button>'
})
app.mount('#app');
</script>
</body>
</html>
在浏览器中点击“访问根实例”按钮,在 Console 窗口中的输出如下:
单价:188
总价:1880
葵花宝典
不管组件是根实例的子组件,还是更深层次的后代组件,root 属性总是代表了根实例。
2. 访问父组件实例
与 root 类似, parent 属性用于在一个子组件中访问父组件的实例,这可以代替父组件通过 prop 想子组件传递数据的方式。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<parent></parent>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({});
app.component('parent', {
data() {
return {
price: 188
}
},
computed: {
totalPrice() {
return this.price * 10;
}
},
methods: {
hello() {
return "重生之门";
}
},
template: '<child></child>'
})
app.component('child', {
methods: {
accessParent() {
console.log("单价:" + this.$parent.price);
console.log("总价:" + this.$parent.totalPrice);
console.log(this.$parent.hello());
}
},
template: '<button @click="accessParent">访问父组件实例</button>'
})
app.mount('#app')
</script>
</body>
</html>
parent 属性只能用于访问父组件实例,如果父组件之上还有父组件,那么该组件是访问不到的。
3. 访问子组件实例或子元素
父组件要访问子组件实例或子元素,可以给子组件或子元素添加一个特殊的属性 ref,为子组件或子元素分配一个引用 ID ,然后父组件就可以通过 refs 属性访问子组件实例或子元素。
如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<parent></parent>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({});
app.component('parent', {
mounted() {
// 访问子元素<input>,让其具有焦点
this.$refs.inputElement.focus();
// 访问子组件<child>的message数据属性
console.log(this.$refs.childComponent.message)
},
template: `
<div>
<input ref="inputElement"><br> <!--子元素-->
<child ref="childComponent"></child> <!-- 子组件-->
</div>`
})
app.component('child', {
data() {
return {
message: 'Java无难事'
}
},
template: '<p>{{message}}</p>'
})
app.mount('#app');
</script>
</body>
</html>
需要注意的是,refs属性只在组件渲染完成之后生效,并且它们不是响应式的。要避免在模板和计算属性中访问 refs。
4. provide 和 inject
root 属性用于访问根实例,parent 属性用于访问父组件实例,但如果组件嵌套的层级不确定,某个组件的数据或方法需要被后代组件所访问,又该如何实现?
此时需要用到两个新的实例选项:provide 和 inject 。
provide 选项允许指定要提供给后代组件的数据或方法,在后代组件中使用 inject 选项接收要添加到该实例中的特定属性。
如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<parent></parent>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({});
app.component('parent', {
data() {
return {
msg: 'Java无难事'
}
},
methods: {
sayHello(name) {
console.log("Hello, " + name);
}
},
provide() {
return {
// 数据message和sayHello方法可供后代组件访问
message: this.msg,
hello: this.sayHello
}
},
template: '<child/>',
})
app.component('child', {
// 接收message数据属性和hello方法
inject: ['message', 'hello'],
mounted() {
// 当自身的方法来访问
this.hello('zhangsan');
},
// 当自身的数据属性来访问
template: '<p>{{message}}</p>'
})
const vm = app.mount('#app')
</script>
</body>
</html>
使用 provide 和 inject ,父组件不需要知道哪些后代组件要使用他提供的属性,后代组件不需要知道被注入的属性来自哪里。
不过上述代码也存在一些问题。首先,注入的 message 属性并不是响应式的,当修改父组件的 msg 数据属性时,message 属性并不会跟着改变。
这是因为默认情况下,provide/inject 绑定的并不是响应式的,可以通过传递 ref属性或 reactive 对象更改这个行为。
其次,provide 和 inject 将应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。
如果数据需要在多个组件中访问,并且能够响应更新,可以考虑真正的状态管理解决方案-Vuex。