Vue 3 有什么新功能?
已发表: 2022-03-10随着 Vue 3 的发布,开发人员必须从 Vue 2 进行升级,因为它带有一些新功能,这些新功能非常有助于构建易于阅读和可维护的组件,并改进了在 Vue 中构建应用程序的方法。 我们将在本文中介绍其中的一些功能。
在本教程结束时,读者将;
- 了解
provide / inject
以及如何使用它。 - 对 Teleport 及其使用方法有基本的了解。
- 了解 Fragments 以及如何使用它们。
- 了解对 Global Vue API 所做的更改。
- 了解对事件 API 所做的更改。
本文面向对 Vue 2.x 有一定了解的人。 您可以在 GitHub 中找到此示例中使用的所有代码。
provide / inject
在 Vue 2.x 中,我们使用props
可以轻松地将数据(字符串、数组、对象等)从父组件直接传递到其子组件。 但是在开发过程中,我们经常会发现需要将数据从父组件传递到深度嵌套的组件的情况,而使用props
则更难做到这一点。 这导致了 Vuex Store、Event Hub 的使用,有时还通过深度嵌套的组件传递数据。 让我们看一个简单的应用程序;
需要注意的是,Vue 2.2.0 还附带了不建议在通用应用程序代码中使用的provide / inject
。
# parentComponent.vue <template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png" /> <HelloWorld msg="Vue 3 is liveeeee!" :color="color" /> <select name="color" v-model="color"> <option value="" disabled selected> Select a color</option> <option :value="color" v-for="(color, index) in colors" :key="index">{{ color }}</option></select > </div> </template> <script> import HelloWorld from "@/components/HelloWorld.vue"; export default { name: "Home", components: { HelloWorld, }, data() { return { color: "", colors: ["red", "blue", "green"], }; }, }; </script>
# childComponent.vue <template> <div class="hello"> <h1>{{ msg }}</h1> <color-selector :color="color"></color-selector> </div> </template> <script> import colorSelector from "@/components/colorComponent.vue"; export default { name: "HelloWorld", components: { colorSelector, }, props: { msg: String, color: String, }, }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> h3 { margin: 40px 0 0; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } </style>
# colorComponent.vue <template> <p :class="[color]">This is an example of deeply nested props!</p> </template> <script> export default { props: { color: String, }, }; </script> <style> .blue { color: blue; } .red { color: red; } .green { color: green; } </style>
在这里,我们有一个登录页面,其中包含一个包含颜色列表的下拉列表,并且我们将选定的color
作为道具传递给childComponent.vue
。 这个子组件还有一个msg
属性,它接受要在模板部分显示的文本。 最后,该组件有一个子组件( colorComponent.vue
),它接受来自父组件的color
属性,用于确定该组件中文本的类。 这是通过所有组件传递数据的示例。
但是在 Vue 3 中,我们可以使用新的提供和注入对以更简洁、更简洁的方式做到这一点。 顾名思义,我们使用provide
作为函数或对象,以使数据从父组件可用于其任何嵌套组件,而不管此类组件的嵌套有多深。 我们在传递硬编码值时使用对象形式来provide
这样的;
# parentComponent.vue <template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png" /> <HelloWorld msg="Vue 3 is liveeeee!" :color="color" /> <select name="color" v-model="color"> <option value="" disabled selected> Select a color</option> <option :value="color" v-for="(color, index) in colors" :key="index">{{ color }}</option></select > </div> </template> <script> import HelloWorld from "@/components/HelloWorld.vue"; export default { name: "Home", components: { HelloWorld, }, data() { return { colors: ["red", "blue", "green"], }; }, provide: { color: 'blue' } }; </script>
但是对于需要传递组件实例属性来provide
的实例,我们使用函数模式,所以这是可能的;
# parentComponent.vue <template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png" /> <HelloWorld msg="Vue 3 is liveeeee!" /> <select name="color" v-model="selectedColor"> <option value="" disabled selected> Select a color</option> <option :value="color" v-for="(color, index) in colors" :key="index">{{ color }}</option></select > </div> </template> <script> import HelloWorld from "@/components/HelloWorld.vue"; export default { name: "Home", components: { HelloWorld, }, data() { return { selectedColor: "blue", colors: ["red", "blue", "green"], }; }, provide() { return { color: this.selectedColor, }; }, }; </script>
由于我们在childComponent.vue
和colorComponent.vue
中都不需要color
属性,所以我们要去掉它。 使用provide
的好处是父组件不需要知道哪个组件需要它提供的属性。
为了在这种情况下需要它的组件中使用它,我们在colorComponent.vue
中这样做;
# colorComponent.vue <template> <p :class="[color]">This is an example of deeply nested props!</p> </template> <script> export default { inject: ["color"], }; </script> <style> .blue { color: blue; } .red { color: red; } .green { color: green; } </style>
在这里,我们使用inject
接收组件所需的变量数组。 在这种情况下,我们只需要color
属性,所以我们只传递它。 之后,我们可以像使用道具时一样使用color
。
我们可能会注意到,如果我们尝试使用下拉菜单选择新颜色,颜色不会在colorComponent.vue
中更新,这是因为默认情况下, provide
中的属性不是响应式的。 为了解决这个问题,我们使用了computed
方法。
# parentComponent.vue <template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png" /> <HelloWorld msg="Vue 3 is liveeeee!" /> <select name="color" v-model="selectedColor"> <option value="" disabled selected> Select a color</option> <option :value="color" v-for="(color, index) in colors" :key="index">{{ color }}</option></select > </div> </template> <script> import HelloWorld from "@/components/HelloWorld.vue"; import { computed } from "vue"; export default { name: "Home", components: { HelloWorld, }, data() { return { selectedColor: "", todos: ["Feed a cat", "Buy tickets"], colors: ["red", "blue", "green"], }; }, provide() { return { color: computed(() => this.selectedColor), }; }, }; </script>
在这里,我们导入computed
并传递我们的selectedColor
,以便它可以在用户选择不同的颜色时进行响应和更新。 当您将变量传递给计算方法时,它会返回一个具有value
的对象。 这个属性保存你的变量的值,所以对于这个例子,我们必须更新colorComponent.vue
看起来像这样;
# colorComponent.vue <template> <p :class="[color.value]">This is an example of deeply nested props!</p> </template> <script> export default { inject: ["color"], }; </script> <style> .blue { color: blue; } .red { color: red; } .green { color: green; } </style>
在这里,我们将color
更改为color.value
以表示使用computed
方法使color
反应后的变化。 此时,每当父组件中的selectedColor
发生变化时,该组件中文本的class
总是会发生变化。
传送
在某些情况下,由于应用程序使用的逻辑,我们创建组件并将它们放置在应用程序的一部分中,但旨在显示在应用程序的另一部分中。 一个常见的例子是用于显示和覆盖整个屏幕的模式或弹出窗口。 虽然我们可以在此类元素上使用 CSS 的position
属性来解决此问题,但使用 Vue 3,我们也可以使用 Teleport。
Teleport 允许我们将组件从其在文档中的原始位置中取出,从默认的#app
容器中包装 Vue 应用程序并将其移动到正在使用的页面上的任何现有元素。 一个很好的例子是使用 Teleport 将 header 组件从#app
div 内部移动到header
重要的是要注意,您只能 Teleport 到存在于 Vue DOM 之外的元素。
Teleport 组件接受两个决定该组件行为的道具,它们是;
-
to
该道具接受类名、id、元素或 data-* 属性。 我们还可以通过传递一个:to
属性来使这个值动态化,而不是动态to
改变 Teleport 元素。 -
:disabled
此道具接受Boolean
,可用于切换元素或组件上的传送功能。 这对于动态更改元素的位置很有用。
使用 Teleport 的理想示例如下所示;
# index.html** <!DOCTYPE html> <html lang="en"> <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> <!-- add container to teleport to --> <header class="header"></header> <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></div> <!-- built files will be auto injected --> </body> </html>
在您的 Vue 应用程序的默认index.html
文件中,我们添加了一个header
元素,因为我们希望将我们的 header 组件传送到我们的应用程序中的那个点。 我们还为这个元素添加了一个类,用于样式化和在我们的 Teleport 组件中轻松引用。
# Header.vue** <template> <teleport to="header"> <h1 class="logo">Vue 3 </h1> <nav> <router-link to="/">Home</router-link> </nav> </teleport> </template> <script> export default { name: "app-header", }; </script> <style> .header { display: flex; align-items: center; justify-content: center; } .logo { margin-right: 20px; } </style>
在这里,我们创建标题组件并添加一个带有指向我们应用程序主页链接的徽标。 我们还添加了 Teleport 组件并为to
属性赋予header
值,因为我们希望该组件在该元素内呈现。 最后,我们将这个组件导入到我们的应用程序中;
# App.vue <template> <router-view /> <app-header></app-header> </template> <script> import appHeader from "@/components/Header.vue"; export default { components: { appHeader, }, }; </script>
在这个文件中,我们导入 header 组件并将其放置在模板中,以便它可以在我们的应用程序中可见。
现在,如果我们检查应用程序的元素,我们会注意到我们的 header 组件位于header
元素内;
碎片
使用 Vue 2.x,文件template
中不可能有多个根元素,作为一种解决方法,开发人员开始将所有元素包装在一个父元素中。 虽然这看起来不是一个严重的问题,但在某些情况下,开发人员希望在没有容器包裹这些元素的情况下渲染组件,但不得不这样做。
在 Vue 3 中,引入了一个名为 Fragments 的新功能,该功能允许开发人员在其根模板文件中拥有多个元素。 所以在 Vue 2.x 中,输入字段容器组件是这样的;
# inputComponent.vue <template> <div> <label :for="label">label</label> <input :type="type" : :name="label" /> </div> </template> <script> export default { name: "inputField", props: { label: { type: String, required: true, }, type: { type: String, required: true, }, }, }; </script> <style></style>
在这里,我们有一个简单的表单元素组件,它接受两个 props, label
和type
,并且这个组件的模板部分被包装在一个 div 中。 这不一定是问题,但如果您希望标签和输入字段直接位于form
元素内。 使用 Vue 3,开发人员可以轻松地将此组件重写为如下所示;
# inputComponent.vue <template class="testingss"> <label :for="label">{{ label }}</label> <input :type="type" : :name="label" /> </template>
对于单个根节点,属性始终归属于根节点,它们也称为非道具属性。 它们是传递给组件的事件或属性,在props
或emits
中没有定义相应的属性。 此类属性的示例是class
和id
。 但是,需要明确定义多根节点组件中的哪些元素应归属于。
这就是使用上面的inputComponent.vue
的含义;
- 在父组件中向该组件添加
class
时,必须指定class
将归属于哪个组件,否则该属性无效。
<template> <div class="home"> <div> <input-component class="awesome__class" label="name" type="text" ></input-component> </div> </div> </template> <style> .awesome__class { border: 1px solid red; } </style>
当您在未定义属性应归属于何处的情况下执行此类操作时,您会在控制台中收到此警告;
并且border
对组件没有影响;
- 要解决此问题,请在您希望将此类属性分发到的元素上添加一个
v-bind="$attrs"
;
<template> <label :for="label" v-bind="$attrs">{{ label }}</label> <input :type="type" : :name="label" /> </template>
在这里,我们告诉 Vue 我们希望将属性分配给label
元素,这意味着我们希望将awesome__class
应用于它。 现在,如果我们在浏览器中检查我们的元素,我们会看到该类现在已添加到label
中,因此标签周围现在有一个边框。
全局 API
在 Vue 应用程序的main.js
文件中看到Vue.component
或Vue.use
并不少见。 这些类型的方法被称为全局 API,在 Vue 2.x 中有相当多的方法。 这种方法的挑战之一是它不可能将某些功能隔离到您的应用程序的一个实例(如果您的应用程序中有多个实例)而不影响其他应用程序,因为它们都安装在 Vue 上。 这就是我的意思;
Vue.directive('focus', { inserted: el => el.focus() }) Vue.mixin({ /* ... */ }) const app1 = new Vue({ el: '#app-1' }) const app2 = new Vue({ el: '#app-2' })
对于上面的代码,不可能说 Vue 指令与app1
相关联,而 Mixin 与app2
相关联,但它们在两个应用程序中都可用。
Vue 3 附带了一个新的 Global API,试图通过引入createApp
来解决此类问题。 此方法返回 Vue 应用程序的新实例。 应用程序实例公开了当前全局 API 的子集。 有了这个,所有从 Vue 2.x 改变Vue
的 API(组件、mixin、指令、使用等)现在都将被移动到单独的应用程序实例中,现在,你的 Vue 应用程序的每个实例都可以具有独特的功能它们不会影响其他现有应用程序。
现在,上面的代码可以重写为;
const app1 = createApp({}) const app2 = createApp({}) app1.directive('focus', { inserted: el => el.focus() }) app2.mixin({ /* ... */ })
但是,可以创建您希望在所有应用程序之间共享的功能,这可以通过使用工厂功能来完成。
事件 API
除了使用 Vuex 存储之外,开发人员在没有父子关系的组件之间传递数据的最常见方法之一是使用事件总线。 这种方法很常见的原因之一是因为它很容易上手。
# eventBus.js const eventBus = new Vue() export default eventBus;
在此之后,接下来就是将此文件导入main.js
以使其在我们的应用程序中全局可用,或者将其导入您需要的文件中;
# main.js import eventBus from 'eventBus' Vue.prototype.$eventBus = eventBus
现在,您可以像这样发出事件并监听发出的事件;
this.$eventBus.$on('say-hello', alertMe) this.$eventBus.$emit('pass-message', 'Event Bus says Hi')
有很多 Vue 代码库都充满了这样的代码。 但是,使用 Vue 3,这是不可能的,因为$on
、 $off
和$once
都已被删除,但$emit
仍然可用,因为子组件需要向其父组件发出事件。 另一种方法是使用provide / inject
或任何推荐的第三方库。
结论
在本文中,我们介绍了如何使用provide / inject
对将数据从父组件向下传递到深度嵌套的子组件。 我们还研究了如何将组件从应用程序中的一个点重新定位和传输到另一个点。 我们研究的另一件事是多根节点组件以及如何确保我们分配属性以便它们正常工作。 最后,我们还介绍了对事件 API 和全局 API 的更改。
更多资源
- “使用 ES6+ 的 JavaScript 工厂函数”,Eric Elliott,Medium
- “使用事件总线在 Vue 组件之间共享道具”,Kingsley Silas,CSS-Tricks
- 在同一目标上使用多个 Teleports,Vue.js 文档
- 非道具属性,Vue.js 文档
- 使用反应性,Vue.js 文档
teleport
, Vue.js 文档- 片段,Vue.js 文档
- 2.x 语法,Vue.js 文档