Bootstrap

前端_Vue_9.模板引用、组件基础


一、模板引用

虽然Vue的声明性渲染模型为你抽象了大部分对DOM的直接操作,但在某些情况下,我们仍然需要直接访问底层DOM元素。

声明性渲染
关于声明性,前期遇到时已经做过简单介绍。而这边官方文档的语句再次暴露了声明性渲染的一些特点,即抽象了大部分对DOM的直接操作,简单讲就是我们只要指定模板挂载到哪里,而不需要直接在操作模板中各个元素的细节(因为这部分在底层或者说后台已经做好了)。

要实现这一点,我们可以使用特殊的 ref attribute:

<input ref="input">

ref 是一个特殊的attribute,和 v-for 章节中提到的 key 类似。它允许我们在一个特定的DOM元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个input元素上,或在一个元素上初始化一个第三方库。

1.1. 访问模板引用

挂载结束后引用都会暴露在 this.$refs 之上:

<script>
export default {
	mounted() {
		this.$refs.input.focus()
	}
}
</script>

<template>
	<input ref="input" />
</template>

注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 $refs.input ,在初次渲染时会是 null 。这是因为在初次渲染前这个元素还不存在!

1.2. v-for 中的模板引用

当在 v-for 中使用模板引用时,相应的引用中包含的值是一个数组:

<script>
export default {
	data() {
		return {
			list: [
				/* ... */
			]
		}
	},
	mounted() {
		console.log(this.$refs.items)
	}
}
</script>

<template>
	<ul>
		<li v-for="item in list" ref="items">
			{{ item }}
		</li>
	</ul>
</template>

应该注意的是,ref 数组并不保证与源数组相同的顺序。

1.3. 函数模板引用

除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:

<input :ref="(el) => { /* 将el赋值给一个数据属性或 ref 变量 */ }" >

注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null 。你当然也可以绑定一个组件方法而不是内联函数。

1.4. 组件上的 ref

模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例:

<script>
import Child from './Child.vue'

export default {
	components: {
		Child
	},
	mounted() {
		// this.$refs.child 是 <Child />组件的实例
	}
}
</script>

<template>
	<Child ref="child" />
</template>

如果一个子组件使用的是选项式API,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。

expose 选项可以用于限制对子组件实例的访问:

export default {
	expose: ['publicData', 'publicMethod'],
	data() {
		return {
			publicData: 'foo',
			privateData: 'bar'
		}
	},
	methods: {
		publicMethod() {
			/* ... */
		},
		privateMethod() {
			/* ... */
		}
	}
}

在上面这个例子中,父组件通过模板引用访问到子组件实例后,仅能访问 publicData 和 publicMethod 。

1.5. 小结

看完模板引用章节后,对模板引用小结一下。

  • 从写法上来讲, ref attribute 就是模板引用,它可以获得挂载后的DOM元素或组件实例的引用。它是用于访问底层模板中的DOM元素的。

    你可能会问“我加个id=‘xxx’,不就可以通过id访问了?”
    这个问题先不回答行或不行。
    其实在Vue中获取DOM的方法并不止一种,但官方推荐的是这种。所以现阶段,使用这种即可。

二、组件基础 ⭐

组件允许我们将UI划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:
在这里插入图片描述
这和我们嵌套 HTML 元素的方式类似, Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。 Vue 同样也能很好地配合原生 Web Component。

2.1. 定义一个组件

当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件(简称SFC):

<script>
export default {
	data() {
		return {
			count: 0
		}
	}
}
</script>

<template>
	<button @click="count++">You clicked me {{ count }} times.</button>
</template>

当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:

export default {
	data() {
		return {
			count: 0
		}
	},
	template:`
		<button @click="count++">
			You clicked me {{ count }} times.
		</button>`
}

这里的模板是一个内联的 JavaScript 字符串,Vue将会在运行时编译它。你也可以使用ID选择器来指向一个元素(通常是原生的 <template> 元素),Vue将会使用其内容作为模板来源。

上面的例子中定义了一个组件,并在一个 .js 文件里默认导出了它自己,但你也可以通过具名导出在一个文件中导出多个组件。

2.2. 使用组件

提示
接下来的指引中都使用SFC语法,无论你是否使用构建步骤,组件相关的概念都是相同的。

要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue 的文件中,这个组件将会以默认导出的形式被暴露给外部。

<script>
import ButtonCounter from './ButtonCounter.vue'

export default {
	components: {
		ButtonCounter
	}
}
</script>

<template>
	<h1>Here is a child component!</h1>
	<ButtonCounter />
</template>

若要将导出的组件暴露给模板,我们需要在 components 选项上注册它。这个组件将会以注册时的名字作为模板中的标签名。

当然,你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入(import)。关于组件的全局注册和局部注册两种方式的利弊,后面组件注册章节中专门讨论。

组件可以被重用任意多次:

<h1>Here is a child component!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />

你会注意到,每当点击这些按钮时,每一个组件都维护着自己的状态,是不同的 count。这是因为每当你使用一个组件,就创建了一个新的实例。

在单文件组件中,推荐为子组件使用 PascalCase(就是大写开头,不同单词间大写区分的命名方法) 的标签名,以此来和原生的 HTML 元素作区分。虽然原生HTML标签名是不区分大小写的,但 Vue 单文件组件是可以在编译中区分大小写的。我们也可以用 />来关闭一个标签。

如果你是直接在DOM中书写模板(例如原生 <template>元素的内容),模板的编译需要遵从浏览器中HTML的解析行为。在这种情况下,你应该需要使用 kebab-case 形式并显式地关闭这些组件的标签。

<!-- 如果是在 DOM 中书写该模板 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>

2.3. 传递props

从命名上来看,prop大概是property的缩写,可以认为是属性的意思。

如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有的博客文章共享相同的视觉布局,但内容不同。要实现这样的效果自然必须向组件中传递数据,例如每篇文章标题和内容,这就会用到props。

Props是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的props列表上声明它。这里要用到 props 选项:

<!-- BlogPost.vue -->
<script>
export default {
	props: ['title']
}
</script>

<template>
<h4>{{ title }}</h4>
</template>

当一个值被传递给 prop 时,它将成为该组件实例上的一个属性。该属性的值可以像其他组件属性一样,在模板和组件的 this 上下文中访问。

一个组件可以有任意多的 props ,默认情况下,所有 prop 都接受任意类型的值。

当一个 prop 被注册后,可以像这样以自定义 attribute 的形式传递数据给它:

<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

在实际应用中,我们可能在父组件中会有如下的一个博客文章数组:

export default {
	// ...
	data() {
		return {
			posts: [
				{ id: 1, title: 'My journey with Vue' },
				{ id: 2, title: 'Blogging with Vue' },
				{ id: 3, title: 'Why Vue is so fun' }
			]
		}
	}
}

这种情况下,我们可以使用 v-for 来渲染它们:

<!--template-->
<BlogPost
	v-for="post in posts"
	:key="post.id"
	:title="post.title"
	/>

留意我们是如何使用 v-bind 来传递动态prop值的。当事先不知道要渲染的确切内容时,这一点特别有用。

2.4. 监听事件

继续关注上面的 <BlogPost>组件。有时候我们需要它和父组件进行交互。例如,要在此处实现A11y(Accessibility )的需求,将博客文章的文字放大,而页面的其余部分仍使用默认字号。

在父组件中,我们可以添加一个 postFontSize 数据属性来实现这个效果:

data() {
	return {
		posts: [
			/* ... */
		],
		postFontSize: 1
	}
}

在模板中用它来控制所有博客文章的字体大小:

<!--template-->
<div :style="{ fontSize: postFontSize + 'em' }">
	<BlogPost
		v-for="post in posts"
		:key="post.id"
		:title="post.title"
	/>
</div>

然后,给 <BlogPost>组件添加一个按钮:

<!--BlogPost.vue,省略了<script>-->
<template>
	<div class="blog-post">
		<h4>{{ title }}</h4>
		<button>Enlarge text</button>
	</div>
</template>

这个按钮目前还没有做任何事情,我们想要点击这个按钮开告诉父组件它应该放大所有博客文章的文字。要解决这个问题,组件实例提供了一个自定义事件系统。父组件可以通过 v-on 或 @ 来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:

<BlogPost
	...
	@enlarge-text="postFontSize += 0.1"
	/>

子组件可以通过调用内置的 $emit 方法,通过传入事件名称来抛出一个事件:

<!-- BlogPost.vue,省略了<script> -->
<template>
	<div class="blog-post">
		<h4>{{ title }}</h4>
		<button @click="$emit('enlarge-text')">Enlarge text</button>
	</div>
</template>

因为有了 @enlarge-text=“postFontSize += 0.1”的监听,父组件会接收这一事件,从而更新 postFontSize 的值。

我们可以通过 emits 选项来声明需要抛出的事件:

<!-- BlogPost.vue -->
<script>
export default {
	props: ['title'],
	emits: ['enlarge-text']
}
</script>

这声明了一个组件可能触发的所有事件,还可以对事件的参数进行验证。同时,这还可以让Vue避免将它们作为原生事件监听器隐式地应用于子组件的根元素(?)。

总之,子组件上声明事件和触发事件;父组件上监听事件即可。

2.5. 通过插槽(slot)来分配内容

一些情况下,我们会希望能和HTML元素一样向组件中传递内容:

<AlertBox>
	Something bad happened.
</AlertBox>

我们期望能渲染成这样:
在这里插入图片描述
这可以通过 Vue 的自定义<slot>元素来实现:

<template>
	<div class="alert-box">
		<strong>This is an Error for Demo Purposes</strong>
		<slot />
	</div>
</template>

<style scoped>
.alert-box {
	/* ... */
}
</style>

如上所示,我们使用 <slot> 作为一个占位符,父组件传递进来的内容就会渲染在这里。

2.6. 动态组件

有些场景会需要在两个组件间来回切换,比如Tab界面:

<script>
import Home from './Home.vue'
import Posts from './Posts.vue'
import Archive from './Archive.vue'
export default {
  components: {
    Home,
    Posts,
    Archive
  },
  data() {
    return {
      currentTab: 'Home',
      tabs: ['Home', 'Posts', 'Archive']
    }
  }
}
</script>

<template>
  <div class="demo">
    <button
       v-for="tab in tabs"
       :key="tab"
       :class="['tab-button', { active: currentTab === tab }]"
       @click="currentTab = tab"
     >
      {{ tab }}
    </button>
	  <component :is="currentTab" class="tab"></component>
  </div>
</template>

在这里插入图片描述
上面例子是通过 Vue 的 <component>元素和特殊的 is attribute 实现的:

<!-- currentTab 改变时组件也改变 -->
<component :is="currentTab"></component>

在上面例子中,被传给 :is 的值可以是以下几种:

  • 被注册的组件名
  • 导入的组件对象

你也可以使用 is attribute 来创建一般的 HTML 元素。
当使用 <component :is=“…”> 来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 <keepAlive> 组件强制被切换掉的组件仍然保持 “存活” 的状态。

2.7. DOM模板解析注意事项 🔺

如果你想在 DOM 中直接书写 Vue 模板,Vue则必须从 DOM 中获取模板字符串。由于浏览器的原生 HTML 解析行为限制,有一些需要注意的事项。

提示
下面讨论只适用于直接在DOM中编写模板的情况。如果你使用来自以下来源的字符串模板,就不需要顾虑这些限制了:

  • 单文件组件
  • 内联模板字符串(例如 template: ‘…’)
  • <scirpt type=“text/x-template”>

由于前期学习使用的是单文件组件,所以本节内容暂时略过,后期有需要再回来学习。

2.7.1. 大小写区分

HTML 标签和属性名称是不分大小写的,所以浏览器会把任何大写的字符解释为小写。这意味着当你使用DOM内的模板时,无论是 PascalCase 形式的组件名称、camelCase形式的prop名称还是 v-on 的事件名称,都需要转换为相应等价的 kebab-case(短横线连字符)形式:

// JavaScript 中的 camelCase
const BlogPost = {
	props: ['postTitle'],
	emits: ['updatePost'],
	template:`
		<h3>{{ postTitle }}</h3>
	`
}
<!-- HTML 中的 kebab-case -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>

2.7.2. 闭合标签

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;