共计 6556 个字符,预计需要花费 17 分钟才能阅读完成。
Vue 3 尚未正式发布,但是维护者已经发布了 Beta 版本,供我们的参与者尝试并提供反馈。
如果你想知道 Vue 3 的主要功能和主要变化是什么,那么我将在这篇文章中重点介绍一下,告诉你使用 Vue 3 beta 9 创建一个简单的应用程序。
我将介绍尽可能多的新内容,包括 fragments,teleport,Composition API 以及其他一些晦涩的更改。我将尽力解释该功能或更改的原理。
我们将建立什么
我们将构建一个带有模式窗口功能的简单应用。我之所以选择它,是因为它可以方便地展示 Vue 3 的许多变化。
这是该应用在打开和关闭状态下的外观,因此你可以在脑海中描绘出我们正在做什么:
Vue 3 安装和 setup
与其直接安装 Vue 3,不如克隆一个项目 vue-next-webpack-preview
,这将为我们提供一个包括 Vue 3 在内的最小的 Webpack 设置。
$ git clone https://github.com/vuejs/vue-next-webpack-preview.git vue3-experiment | |
$ cd vue3-experiment | |
$ npm i |
一旦克隆好了,安装好了 NPM 模块,我们需要做的就是删除样板文件,然后创建一个新的 main.js
文件,这样我们就可以从头开始创建我们的 Vue 3 app 了。
$ rm -rf src/* | |
$ touch src/main.js |
现在,我们将运行开发服务器:
$ npm run dev
创建一个新的 Vue 3 app
我们启动一个新的 Vue 应用程序的方式改变了,我们现在需要导入新的 createApp
方法,而不是使用新的 Vue()
。
我们调用这个方法,传递我们的 Vue 实例定义对象,并将返回对象分配给一个变量 app
。
接下来,我们将在 app
上调用 mount
方法,并传递一个 CSS 选择器来指示我们的 mount 元素,就像在 Vue 2 中使用 $mount
实例方法一样。
// src/main.js | |
import {createApp} from "vue"; | |
const app = createApp({// 根实例定义}); | |
app.mount("#app"); |
变化的原因
与旧的 API 一样,我们添加的任何全局配置(plugins,mixins,原型属性等)都将永久更改全局状态。例如:
// src/main.js | |
// 影响两个实例 | |
Vue.mixin({...}) | |
const app1 = new Vue({el: '#app-1'}) | |
const app2 = new Vue({el: '#app-2'}) |
在单元测试中,这确实是一个问题,因为要确保将每个测试都与上一个测试隔离是很棘手的。
在新的 API 下,调用 createApp
将返回一个新的 app 实例,该实例不会被应用于其他实例的任何全局配置污染。
了解更多:Global API change RFC。
添加 state 属性
我们的模态窗口可以处于两种状态之一——打开或关闭。让我们用一个布尔状态属性 modalOpen
来管理它,我们将给它一个初始值 false
。
在 Vue 2 下,我们可以通过在我们的应用实例上创建一个 data
属性并将一个对象分配给该对象来声明 modalOpen
属性,例如:
// src/main.js | |
const app = createApp({ | |
data: {modalOpen: false} | |
}); |
不再允许这样做。相反,必须为数据分配一个返回状态对象的工厂函数。
// src/main.js | |
const app = createApp({data: () => ({modalOpen: false}) | |
}); |
变化的原因
使用对象而不是工厂函数来存储数据的优点是,首先,它在语法上更简单;其次,你可以在多个根实例之间共享顶级状态,例如:
// src/main.js | |
const state = {sharedVal: 0}; | |
const app1 = new Vue({state}); | |
const app2 = new Vue({state}); | |
// 影响两个实例 | |
app1._data.sharedVal = 1; |
这种用例很少,可以使用。因为有两种类型的声明是不适合初学者的,所以决定删除这个特性。
了解更多:Data object declaration removed RFC
在继续之前,我们还添加一个方法来切换 modalOpen
值。这与 Vue 2 没什么不同。
// src/main.js | |
const app = createApp({data: () => ({modalOpen: true}), | |
methods: {toggleModalState() {this.modalOpen = !this.modalOpen;} | |
} | |
}); |
使用一个根组件
如果你现在进入浏览器并检查控制台,则会看到警告“Component is missing render function”,因为我们尚未为根实例定义模板。
Vue 2 的最佳实践是为根实例创建一个最小的模板,并创建一个 app 组件,其中将声明主 app 标记。
让我们在这里也这样做。
$ touch src/App.vue
现在我们可以获取根实例来渲染该组件。区别在于,对于 Vue 2,我们通常会使用 render 函数来执行此操作:
// src/main.js | |
import App from "./App.vue"; | |
const app = createApp({ | |
... | |
render: h => h(App) | |
}); | |
app.mount("#app"); |
我们仍然可以做到这一点,但是 Vue 3 有一个更简单的方法——使 App
成为根组件。为此,我们可以删除根实例定义,而是传递 App
组件。
// src/main.js | |
import App from "./App.vue"; | |
const app = createApp(App); | |
app.mount("#app"); |
这意味着 App
组件不仅由根实例渲染,而且是根实例。
在此过程中,我们通过删除 app
变量来简化语法:
// src/main.js | |
createApp(App).mount("#app"); |
现在移至根组件,让我们向该组件重新添加状态和方法:
// src/App.vue | |
<script> | |
export default {data: () => ({modalOpen: true}), | |
methods: {toggleModalState() {this.modalOpen = !this.modalOpen;} | |
} | |
}; | |
</script> |
我们还为模态功能创建一个新组件:
$ touch src/Modal.vue
现在,我们将提供一个最小的模板,其中包括内容插槽。这确保了我们的模态是可重用的。稍后我们将向此组件添加更多内容。
// src/Modal.vue | |
<template> | |
<div class="modal"> | |
<slot></slot> | |
</div> | |
</template> |
多根模板
现在让我们为我们的根组件创建模板。我们将创建一个按钮来打开模态,它将触发 toggleModalState
方法。
我们还将使用我们刚刚创建的 modal 组件,它将根据 modalState
的值来渲染。让我们也在槽中插入一段文字作为内容。
// src/App.vue | |
<template> | |
<button @click="toggleModalState">Open modal</button> | |
<modal v-if="modalOpen"> | |
<p>Hello, I'm a modal window.</p> | |
</modal> | |
</template> | |
<script> | |
import Modal from "./Modal.vue"; | |
export default { | |
components: {Modal}, | |
... | |
} | |
</script> |
注意这个模板有什么奇怪的地方吗?再看一遍。
没错——有两个根元素。在 Vue 3 中,由于有了一个叫做片段(fragments)的功能,它不再强制要求有一个单一的根元素!
使用 Composition API 进行重构
Vue 3 的旗舰功能是 Composition API。这个新的 API 允许你使用 setup
功能而不是使用添加到组件定义对象的属性来定义组件功能。
现在,让我们重构 App 组件以使用 Composition API。
在解释代码之前,请清楚我们所做的只是重构——组件的功能将相同。还要注意,模板没有更改,因为 Composition API 仅影响我们定义组件功能的方式,而不影响我们渲染它的方式。
src/App.vue
<template> | |
<button @click="toggleModalState">Open modal</button> | |
<modal v-if="modalOpen"> | |
<p>Hello, I'm a modal window.</p> | |
</modal> | |
</template> | |
<script> | |
import Modal from "./Modal.vue"; | |
import {ref} from "vue"; | |
export default {setup () {const modalState = ref(false); | |
const toggleModalState = () => {modalState.value = !modalState.value;}; | |
return { | |
modalState, | |
toggleModalState | |
} | |
} | |
}; | |
</script> |
setup 方法
首先,请注意,我们导入了 ref
函数,该函数允许我们定义响应式变量 modalState
。此变量等效于 this.modalState
。
toggleModalState
方法只是一个普通的 JavaScript 函数。但是,请注意,要更改方法主体中的 modalState
值,我们需要更改其子属性 value
。这是因为使用 ref
创建的响应式变量被封装在一个对象中。这对于保留它们的响应式是非常必要的,因为它们在被传递的过程中会被保留下来。
最后,我们从 setup
方法返回 modalState
和 toggleModalState
,因为这些是在呈现模板时传递给模板的值。
变化的原因
请记住,Composition API 并不是更改,因为它纯粹是可选的。主要动机是允许更好的代码组织和组件之间的代码重用(因为 mixin 本质上是一种反模式)。
如果你认为在这个例子中重构 App 组件以使用 Composition API 是没有必要的,那你的想法是正确的。但是,如果这是一个更大的组件,或者我们需要与其他组件共享其功能,那么你就会发现它的用处。
Teleporting content
如果你曾经创建过模态功能,你会知道它通常被放置在关闭的 </body>
标签之前。
<body> | |
<div> | |
<!--main page content here--> | |
</div> | |
<!--modal here--> | |
</body> |
这样做是因为模式通常具有覆盖页面的背景,要使用 CSS 来实现,您不需要处理父元素定位和 z -index 堆栈上下文,因此最简单的解决方案是将模式放在 DOM 的最底部。
但这在 Vue.js 中产生了一个问题,它假定 UI 将作为一个单一的组件树来构建。为了允许将树的片段移动到 DOM 中的其他位置,在 Vue 3 中添加了一个新的 teleport
组件。
要使用 teleport,首先要在页面上添加一个元素,我们要将模态内容移动到该页面。我们将转到 index.html
,并将 ID 为 modal-wrapper
的 div
放在 Vue 的安装元素旁边。
index.html
<body> | |
... | |
<div id="app"></div><!--Vue mounting element--> | |
<div id="modal-wrapper"> | |
<!--modal should get moved here--> | |
</div> | |
</body> |
现在,回到 App.vue
,我们将模态内容包装在 teleport
组件中。我们还需要指定一个 to
属性,为该属性分配一个查询选择器,以标识目标元素,在本例中为 #modal-wrapper
。
src/App.vue
<template> | |
<button @click="toggleModalState">Open modal</button> | |
<teleport to="#modal-wrapper"> | |
<modal v-if="modalOpen"> | |
<p>Hello, I'm a modal window.</p> | |
</modal> | |
</teleport> | |
</template> |
就是这样,teleport
中的任何内容都将渲染在目标元素中。
Emitting 和 event
现在,让我们在 modal 中添加一个按钮,让它可以被关闭。要做到这一点,我们要在 modal 模板中添加一个按钮元素,并添加一个点击处理程序,该处理程序会发出一个 close
事件。
src/Modal.vue
<template> | |
<div class="modal"> | |
<slot></slot> | |
<button @click="$emit('close')">Dismiss</button> | |
</div> | |
</template> |
然后,该事件将由父组件捕获,并将切换 modalState
的值,从逻辑上将其设置为 false
并导致窗口关闭。
src/App.vue
<template> | |
... | |
<modal | |
v-if="modalOpen" | |
@click="toggleModalState" | |
> | |
<p>Hello, I'm a modal window.</p> | |
</modal> | |
</teleport> | |
</template> |
到目前为止,此功能与 Vue 2 中的功能相同。但是,现在在 Vue 3 中,建议您使用新的 emits
组件选项显式声明组件的事件。就像 props 一样,你可以简单地创建一个字符串数组来命名组件将发出的每个事件。
src/Modal.vue
<template>...</template> | |
<script> | |
export default {emits: [ "close"] | |
} | |
</script> |
变化的原因
想象一下,打开别人写的组件的文件,看到它的 prop 和 event 明文声明。马上,你就会明白这个组件的界面,也就是它要发送和接收什么。
除了提供自说明代码外,你还可以使用事件声明来验证你的事件有效载荷,虽然我在这个例子中找不到理由来验证。
了解更多:Emits Option RFC
样式插槽内容
为了使模态可重用,我们提供了一个内容插槽。让我们开始通过为组件添加 style
标签来为内容设置样式。
在我们的组件中使用 scoped
CSS 是一种很好的做法,以确保我们提供的规则不会对页面中的其他内容产生意外影响。
让我们把任何被放入插槽中的段落文字变成斜体。要做到这一点,我们将使用 p
选择器创建一个新的 CSS 规则。
src/Modal.vue
<template>...</template> | |
<script>...</script> | |
<style scoped> | |
p {font-style: italic;} | |
</style> |
如果你尝试一下,你会发现这一点并不奏效。问题是,在编译时,当插槽内容仍属于父对象时,Scoped styling 是在编译时确定的。
Vue 3 提供的解决方案是提供一个伪选择器 ::v-slotted()
,允许你在提供插槽的组件中使用范围化规则来针对插槽内容。
这是我们的用法:
<style scoped> | |
::v-slotted(p) {font-style: italic;} | |
</style> |
Vue 3 还包含了其他一些新的 Scoped Styling 选择器:::v-deep
和 ::v-global
,你可以在这里了解更多:Scoped Styles RFC。
其他改变
好吧,这就是我可以在一个简单示例中涵盖的所有新功能。主要的我基本都有了,但这里有一些我认为很重要的,在总结文章之前,我觉得足够重要,可以自己研究一下。
添加的:
- Global API treeshaking
移出的:
- Filters
- Inline templates
- Event interface for components(不再有 event bus)
更改的:
- Async component API
- Custom directive API
- Render function syntax
关于 Vue Router 也有各种变化,但我将专门用一篇文章来介绍这些变化!
本文首发于公众号《前端外文精选》,关注后私信回复:大礼包,送某网精品视频课程网盘资料,准能为你节省不少钱!