Vue3 初学者指南(三)
原文:
zh.annas-archive.org/md5/f9e4bd72d595e5007a42cab59e1b51c3译者:飞龙
第十章:使用 Vue Router 处理路由
单页应用程序(SPAs),例如 Vue.js 提供的,基于单页面的架构。这种方法防止页面完全重新加载,并提供了改进的用户体验。
随着您的应用程序增长,您将需要在应用程序中创建不同的视图。即使术语 SPA 可能会导致您认为您的应用程序将建立在单个页面上,但事实远非如此。
大多数框架,包括 Vue.js,都提供旨在在其他框架(如 PHP、.NET 等)中重现路由系统的包。SPA 框架提供的路由功能提供了两者的最佳结合。它为开发者提供了一个完整的路由工具包,同时仍然提供了 SPA 框架预期的相同用户体验。Vue.js 中使用的路由包称为 vue-router。
在本章中,您将学习如何在您的 Vue.js 应用程序中使用路由。在本章结束时,您将很好地理解 vue-router 及其配置,以及路由是如何定义和使用的。您将能够创建基本、动态和嵌套路由,并使用不同的方法导航到它们。最后,您将学习如何使用 redirect 和别名来提高用户体验。
我们首先将通过介绍其配置来了解 vue-router。然后,我们将通过创建几个静态页面来学习如何实现我们的第一个路由和路由导航。接下来,我们将通过定义用户个人资料页面来介绍动态路由。然后,我们将通过使用嵌套路由将用户视图拆分为用户个人资料和用户帖子来添加另一个导航级别。最后,我们将通过熟悉 redirect 和 alias 来完成本章。
本章将分为以下部分:
-
介绍 vue-router
-
在路由间导航
-
动态路由匹配
-
嵌套路由
-
使用
alias和redirect重复使用路由
技术要求
在本章中,该分支被称为 CH10。要拉取此分支,请运行以下命令或使用您选择的 GUI 来支持您进行此操作:git switch CH10。
本章的代码文件可以在 github.com/PacktPublishing/Vue.js-3-for-Beginners 找到。
介绍 vue-router
vue-router 是由 Vue.js 核心团队和社区成员构建和维护的官方路由包。就像我们在 Companion App 中介绍的其他包一样,当我们使用 Vite 初始化应用程序时,vue-router 也自动为我们设置好了。
在本节中,我们将了解 vue-router 所需的文件结构和配置,并介绍在处理路由时使用的部分语法。
vue-router 提供了一组标准的、从路由器期望的功能。所以,如果您在其他语言中以前使用过路由器,我们将涵盖的大部分内容听起来可能很熟悉,但阅读它仍然值得,因为语法可能不同。
了解 vue-router 配置
让我们先学习如何最好地配置应用程序中的路由。实际上,即使 vue-router 通常由createVue和Vite等工具预设,了解它在幕后是如何设置的仍然很重要。
插件工作所需的配置存储在一个通常命名为router.js的文件中,或者存储在名为router的文件夹中的index.js文件中。
在我们的案例中,文件存储在router文件夹内。
所有插件都需要在main.js中注册
所有插件必须在main.js中注册后才能工作。因此,如果您想找到当前在您的应用程序中加载的插件的配置文件或信息,您可以打开main.js文件并搜索app.use(pluginName)语法。
让我们看看您可能会在index.js文件中找到的语法:
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [{
path: '/',
name: 'home',
component: HomeView
}]
})
export default router
让我们回顾一下前面配置的重要点。首先,我们可以谈谈createRouter方法。这个由 vue-router 包提供的方法创建了一个可以附加到 Vue 应用程序的 router 实例。此方法期望一个包含 router 配置的对象。
接下来,是时候看看配置对象中的第一个条目,history。history属性定义了您的应用程序如何在不同的页面之间导航。在标准应用程序中,这通常是通过将网站 URL 更改为所需的页面来实现的——例如,通过将/team附加到 URL 来访问团队页面。让我们看看可以用来设置history属性的一些不同的配置。
Hash 模式
这种方法是通过使用createWebHashHistory实现的。这个方法由 vue-router 包提供,并且它是实现起来最简单的一个,因为它不需要任何服务器端配置。当使用 hash 模式时,实际的 hash(#)将被添加到基础 URL 和我们的路由之间。使用这种配置,访问团队页面可以通过访问www.mywebsite.com#team来完成。
这种方法可能会对您的 SEO 产生负面影响,所以如果您的应用程序是公开访问的,您应该投入时间并设置下一个可用的方法,即 web 历史模式。
HTML5/web 历史模式
HTML5 模式可以通过使用createWebHistory方法进行配置。使用这种历史模式,我们的网站将表现得像一个标准网站,其路由将直接在网站 URL 之后提供(例如,www.mywebsite.com/team)。
由于 SPA 网站是建立在单个页面上的(因此得名),它们可以从一个单一的端点(网站基础 URL)提供应用程序。因此,部署我们的网站并直接尝试访问团队页面会导致404页面(未找到)。
在今天的托管网站上解决此问题是一个简单任务,因为所需的一切只是一个通配符规则,确保网站导航被引导到 SPA 入口点。如果您想使用这种方法,稍作搜索就能找到您在托管提供商中正确设置所需的说明。
在我们的情况下,我们使用 Web 历史记录。这是我的默认历史设置,不仅因为它提高了 SEO,而且因为它已经是我们网站导航的正常方式多年了,我喜欢保持一致性。
最后,是时候介绍我们的配置中的最后一个条目:路由。
定义路由
我们已经到达了我们的路由配置中最重要的一部分,即实际的路由。单词 route 定义了我们的应用程序将用户引导到特定页面的能力。因此,当声明路由时,我们定义了用户可以在我们的网站上访问的页面。
要声明一个路由,我们需要两个信息,path 和 component,但我通常更喜欢总是包括第三个称为 name 的信息。
path 属性用于定义需要访问此路由才能加载的 URL。因此,如果用户导航到您网站的基路径,将传递 / 路径,而 /team 路径将可在 www.mysite.com/team 上访问。
component 属性是当访问此路由时预期要加载的 Vue 组件。最后,我们有 name。将此参数添加到所有我们的路由中是良好的实践,因为 name 用于程序化导航路由。我们将在稍后更详细地介绍这一点,因为我们将会学习如何在我们的应用程序内导航。
现在我们已经了解了路由的所有不同方面,让我们尝试解码之前共享的代码片段中声明的路由。这些片段显示了一个路由将用户引导到我们网站的基路径(path 是 /),name 值为 home,这将加载一个 HomeView.vue 组件。
到目前为止,我们已经学习了如何配置 Vue 实例,但在我们的路由能够正常工作之前,还需要一个额外的步骤:将 RouterView 添加到我们的应用程序中。
我们已经定义了给定 URL 要加载的组件,但我们还没有告诉我们的 Vue 应用程序在哪里加载此组件。
路由器的工作方式是在每次用户导航到不同的页面时替换我们应用程序的内容。所以,用非常简单的话说,路由器可以被定义为一个巨大的 if/else 语句,根据 URL 渲染组件。
为了允许路由器正确工作,我们将向我们的应用程序的主要入口点,即 App.vue 文件,添加一个名为 <RouterView> 的组件:
<script setup>
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
从这个阶段开始,vue-router 将接管我们配置中定义的路由定义所显示的屏幕内容。
创建我们的第一个视图
让我们尝试添加一个新的路由导航,用于一个名为 隐私 的静态页面。现在这仅仅包括一些占位文本。
要添加一个新页面,我们需要两个步骤:在我们的routes数组中定义一个路由,以及当访问该路由时将被加载的组件。
用作路由的组件存储在一个名为views的文件夹中。如果我们访问这个文件夹,我们会看到我们目前有两个视图设置,Home和About。
图 10.1:显示视图文件夹内容的文件夹树
我们将在我们的文件夹中添加一个名为PrivacyView.vue的新文件。将文件名与路由的名称匹配是常见的。因为这个文件将是静态的,所以当我们学习插槽时,我们将重用定义在第九章中的布局。
用于定义静态页面的布局StaticTemplate.vue接受三个不同的命名插槽:一个标题、一个页脚以及用于其主内容的默认模板。我们的PrivacyView.Vue文件内容应该定义如下:
<template>
<StaticTemplate>
<template #heading>Privacy Page</template>
<template #default>
This is the content of my privacy page
</template>
</StaticTemplate>
</template>
<script setup>
import StaticTemplate from '../components/templates/StaticTemplate.vue'
</script>
你可能已经注意到,文件内容仅定义了两个槽位(heading 和 default)并且缺少页脚。这是故意为之,因为页脚有一个我们想要显示的默认值。
在我们的新组件可以被访问和显示之前,我们需要将其添加到我们的路由中。让我们通过回到router文件夹内的index.js并添加我们的隐私页面路由到routes数组中来实现这一点:
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'
import PrivacyView from '../views/PrivacyView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: AboutView
},
{
path: '/privacy',
name: 'privacy',
component: PrivacyView
}
]
})
export default router
我们的新页面视图首先作为普通 Vue 组件导入页面的顶部,然后分配给新的路由。正如我们之前提到的,路由需要三个值。首先,我们将path值设置为/privacy,然后我们定义了一个名为privacy的name值,用于未来的程序化导航,最后,我们将导入的PrivacyView组件分配给它。
在这些更改之后,我们应该可以通过访问http://localhost:5173/privacy来看到我们的页面:
图 10.2:隐私页面的截图
在本节中,我们学习了如何配置 vue-router,介绍了路由以及它们如何被应用程序用于渲染页面,最后讨论了<RouterView>以及 vue-router 如何使用它来向我们的访客显示正确的页面。
在下一节中,我们将学习如何在不同的路由之间进行导航。
在路由之间导航
到目前为止,我们已经学习了如何创建我们的路由以及如何通过在浏览器中直接加载 URL 来导航它们。在本节中,我们将学习如何在代码库中直接导航到不同的路由。
确实,我们可以使用简单的<a>标签来定义我们的导航,但这将迫使应用程序在每次导航时完全重新加载,这与 SPA 的整体架构相违背,SPA 提供了一种“无重新加载”的体验。
为了解决这个问题,vue-router 提供了组件和方法来处理导航而不会重新加载页面。
在使用 vue-router 时,可以通过两种不同的方式进行导航。一种使用名为<router-link>的组件,另一种则是通过router.push()程序化触发。让我们看看这两种方法的具体操作,并了解何时使用它们。
使用<router-link>组件
使用<router-link>组件在您的应用内进行导航是一个简单的任务,因为它使用了与原生 HTML <a>元素相同的语法。在幕后,<router-link>只是一个带有附加功能的锚点标签,它阻止应用在导航时完全重新加载。
我们的伴随应用有三个不同的页面,但除非直接输入 URL,否则无法访问它们。现在让我们通过在侧边栏中添加两个链接来修复这个问题,每个链接对应我们迄今为止创建的每个静态页面。
作为提醒,主页是HomePage.vue,但实际的侧边栏只是该页面的一个子组件,可以在organisms文件夹下以SideBar.vue的名称找到。现在我们已经找到了文件,是时候添加我们的路由链接了。
首先,我们在组件的script部分导入 vue-router 组件:
import { RouterLink } from 'vue-router';
接下来,我们在侧边栏中添加链接,直接在Update Time按钮之后:
<TheButton @click="onUpdateTimeClick">
Update Time
</TheButton>
<router-link to="privacy">Privacy</router-link>
<router-link to="about">About</router-link>
router-link组件接受一个名为to的属性。这个属性可以接受浏览器需要导航到的 URL 或route对象,我们将在本章后面介绍。
在我们的示例中,我们已将to属性的值指定为privacy,用于指向about页面,以便导航到关于页面。
在这两项更改之后,应用侧边栏将新增两个链接。
图 10.3:带有路由链接的伴随应用侧边栏
因为路由链接只是简单的<a>元素,它们继承了项目模板中定义的锚点样式。
程序化导航
对于大多数情况,使用路由链接组件进行导航就足够了,但在某些情况下,您可能会发现直接在代码中通过应用进行导航更有益。
使用router-link组件进行导航或程序化导航之间没有真正的区别,这两种方法都提供以确保代码整洁且易于阅读。
程序化导航在逻辑部分使用时很有用。能够手动触发导航可以帮助您控制数据流,确保代码编写良好,用户获得最佳体验。
要导航到不同的页面,我们可以使用router对象提供的一个名为push的方法。这个方法与<router-link>一样,可以接受 URL 或route对象。
我们将添加另一个链接,但这次是一个简单的锚点元素,并使用onclick事件触发导航。
首先,我们将从 vue-router 包中添加一个名为 useRouter 的导入:
import { RouterLink, useRouter } from 'vue-router';
然后,我们使用此方法来访问 router 实例。这只需要在每个文件中做一次:
const router = useRouter();
接下来,我们将创建一个方法来处理我们的锚点的 click 事件:
const navigateToPrivacy = (event) => {
event.preventDefault();
console.log("Run a side effect");
router.push("privacy");
}
通过编程方式触发导航允许我们触发使用 <router-link> 组件时不可能出现的副作用,例如,根据从服务器返回的值有条件地导航用户到不同的页面。
最后,我们将在组件的 HTML 部分创建一个新的锚点。这个元素将使用 navigateToPrivacy 方法:
<a @click="navigateToPrivacy">Programmatic to privacy</a>
到目前为止,我们已经完成了本章的第一部分。在这个阶段,你已经学习了 vue-router 的基础知识,并了解了如何定义路由、它们的工作原理以及如何在不同的页面之间导航。
在接下来的章节中,我们将学习更高级的主题,例如嵌套路由和动态路由匹配。
动态路由匹配
如果你被分配去构建一个简单的个人作品集网站,基本的路由就足够了。然而,当你开始处理更复杂的网站,例如博客时,你需要更复杂的路由。在本节中,我们将学习动态路由匹配。动态路由匹配用于定义需要一个或多个参数来动态化的路由。
我们到目前为止定义的所有路由都是静态的,没有变化。因此,我们有一个 /about 端点,它会渲染 / 路径,欢迎我们的网站主页。如果我们想用当前的知识开发一篇博客文章会发生什么?在我们的第一篇博客文章之后,我们会写一个路由 /blog/1,然后第二篇之后,我们必须创建另一个路由 /blog/2,依此类推。
以这种方式静态定义路由不是一个理想的选择,这正是动态路由匹配发挥作用的地方。动态路由匹配允许我们创建一个模式,例如,blog/:blogId,然后让应用程序渲染一个特定的页面,该页面使用定义的参数来渲染另一个特定的页面。
为了了解这个伟大的功能,我们将通过添加一个新功能来增强我们的伴侣应用程序,该功能将允许我们打开特定的用户个人资料。
这个任务的要求如下:
-
我们将添加用户从应用程序的主视图打开用户个人资料页面的能力
-
我们将创建一个新的路由,用于显示用户信息
-
我们将创建一个新的组件,用于显示包含所有信息的用户
我们将从这个任务的反向开始开发,首先创建用户特定的页面。
创建用户个人资料页面
我们的第一项任务需要我们创建一个新页面,该页面将作为我们路由的用户页面内容。就像所有其他页面一样,它也将位于一个名为 views 的文件夹中,并被称为 UserView.vue。
在这个组件中,我们首先将加载一个特定的用户配置文件,然后在屏幕上显示其信息。为了简化开发,我们将硬编码userId,这样我们目前可以在屏幕上看到正确的信息,之后在完全设置动态路由后将其移除。
就像简单的组件一样,我们需要定义三个不同的部分:HTML、获取正确信息的逻辑以及样式。
为了帮助您为现实世界做好准备,您可以比这本书更进一步,了解我们从 API 需要哪些信息来开发页面。当从外部来源(如 API 或内容管理系统)开发时,user对象(但那将包括什么?会有名字吗?会有出生日期,如果是的话,格式会是什么?)
所有这些问题通常都由user对象回答,该对象通常可以直接从 API 和 CMS 文档中获得。在我们的案例中,由于我们使用dummyapi.com,有关 API 模型的信息可以在这里找到:dummyapi.io/docs/models。
在模型中,我们可以找到我们的特定模型,称为User Full:
图 10.4:用户全信息的模式信息
如图 10**.4所示的模式信息可以帮助我们开发一个结构良好的代码库,并提前做出正确的选择。
现在我们已经很好地理解了 API 将返回的对象,是时候构建我们的组件了。让我们从定义 HTML 部分开始:
<template>
<section class="userView">
<h2>User information</h2>
<template v-for="key in valuesToDisplay">
<label v-if="user[key]" >
{{ key }}
<input
type="text"
disabled
:value="user[key]"
/>
</label>
</template>
</section>
</template>
HTML 标记使用了v-for指令来遍历从 API 接收到的用户对象中预选的属性列表,我们随后使用v-if添加一些额外的验证,以确保我们的应用程序在 API 更改其返回对象的情况下不会崩溃。HTML 将生成一个简单的布局,为每个属性渲染一对标签/输入,但可以进行进一步的开发以改进 UI。
接下来,我们将创建获取和分配我们的用户信息的逻辑。如前所述,我们将硬编码一个 ID 为657a3106698992f50c0a5885,这将在以后被改为动态:
<script setup>
import { reactive } from 'vue';
const user = reactive({});
const valuesToDisplay = [
"title",
"firstName",
"lastName",
"email",
"picture",
"gender"
];
const fetchUser = (userId) => {
const url = `https://dummyapi.io/data/v1/user/${userId}`;
fetch(url, {
"headers": {
"app-id": "1234567890"
}
})
.then( response => response.json())
.then( result => {
Object.assign(user, result.data);
})
}
fetchUser("60d0fe4f5311236168a109ca");
</script>
API 逻辑与其他我们已经在这本书中做出的请求非常相似。唯一的区别如下:
-
url变量不是一个静态值,而是动态的,因为它使用了userId。 -
由于我们需要切换
user的全值,我们必须使用Object.assign。在更改reactive属性的整个值时,这是必需的。 -
valuesToDisplay的值已被设置为简单的数组,并且没有使用ref或reactive。这是故意为之,因为这个数组预计不会被修改,因此不需要是响应式的。在你开始 Vue.js 开发时,最好始终将变量定义为ref/reactive,因为静态的预期可能会很容易变成动态的。
用户资料组件现在已经完成,但它仍然无法从 UI 中访问,因为没有组件或视图来加载它。我们将在下一节中修复这个问题,我们将介绍动态路由。
创建用户资料路由
动态路由的创建遵循与正常路由相同的流程。所有路由都是通过在router/index.js中的路由配置文件中添加到route数组中定义的:
...
import PrivacyView from '../views/PrivacyView.vue'
import UserView from '../views/UserView.vue'
...,
{
path: '/privacy',
name: 'privacy',
component: PrivacyView
},
{
path: "/user/:userId",
name: "user",
component: UserView
}]
就像之前一样,我们向我们的route对象添加了三个不同的属性:name(这将为我们提供一个与路由关联的友好名称),component(这将显示要加载哪个组件——在我们的例子中,是我们新创建的UserView组件),最后是path。
你可能已经注意到,与之前的路由相比,path值有不同的语法,这就是使这条路径成为动态路由的原因。实际上,在之前的例子中,由路由创建的 URL 是唯一的和静态的,而在这个例子中,我们在 URL 中添加了一个名为:的动态部分,位于userId单词之前。因此,这个路径不会翻译成website.com/user/:userId,而是定义了一个期望用值替换:userId的 URL。在我们的例子中,路径可能看起来像website.com/user/1234或website.com/user/my-user-id。
正如我们之前提到的,替换参数的值将在 Vue 组件内部可用,并可用于渲染唯一的页面。在我们的例子中,提供的 ID 将用于加载特定用户的信息,因此访问不同 ID 的 URL 将渲染不同的用户信息页面。
你可以在一个路由中拥有多个参数
你知道你可以在单个路由中拥有多个动态参数吗?每个参数都将使用相同的语法定义。例如,你可以定义一个 URL 为/account/:accountId/user/:userId。将加载此路由的组件将能够访问两个动态参数,一个包含账户 ID,另一个包含用户 ID。
使用路由名称添加导航
现在用户路由已经定义,是时候对我们的设计做一些更改,以确保用户可以导航到它。
即使我们可以通过使用之前章节中学到的导航轻松完成这个任务,我们还是要抓住这个机会来介绍一种新的语法,即使用路由名称进行导航。
在定义新路由时,我们为每个路由项提供了path和name值。使用路径导航对基本 URL 来说很简单,但随着事情变得复杂,可能更干净的做法是使用路由名称来定义导航代码。
在路由系统中为每个路由提供名称是一种高度采用的最佳实践,因为它允许代码更易于阅读和维护。
让我们打开SocialPost.vue并添加导航逻辑到用户头像。为了完成这个任务,我们首先将向<img>元素添加一个click事件:
<img
class="avatar"
:src="img/avatarSrc"
@click="navigateToUser"
/>
我们将名为navigateToUser的方法附加到click事件。这个方法没有参数,因为我们能够直接从component作用域访问所需的一切。
接下来,我们将在组件的<script>块中创建navigateToUser方法:
<script setup >
import { onMounted, ref, computed } from 'vue';
import SocialPostComments from './SocialPostComments.vue';
import IconHeart from '../icons/IconHeart.vue';
import IconDelete from '../icons/IconDelete.vue';
import TheButton from '../atoms/TheButton.vue';
import { useRouter } from 'vue-router';
const showComments = ref(false);
const onShowCommentClick = () => {
console.log("Showing comments");
showComments.value = !showComments.value;
}
const props = defineProps({
username: String,
id: String,
avatarSrc: String,
post: String,
likes: Number
});
const router = useRouter();
const navigateToUser = () => {
router.push({
name: "user",
params: {
userId: props.id
}
});
}
要添加我们的程序化导航,我们首先导入了useRoute方法,然后使用const router = useRouter();初始化路由,最后定义了我们的方法。
使用名称进行导航使用与路径导航相同的router.push方法。区别在于传递给它的值。在之前的例子中,我们只是传递了一个单个字符串,而现在我们传递了一个对象。
此对象包括路由名称和参数列表,在我们的案例中相当于userId:
{
name: "user",
params: {
userId: props.id
}
}
名称和参数都需要与route数组中定义的信息相匹配。这个信息是区分大小写的,所以在将其应用于导航对象时要格外小心。
运行应用程序并点击用户头像将重定向到类似http://localhost:5173/user/60d21b4667d0d8992e610c85的 URL。
在我们庆祝工作完成之前,我们需要做一步,这将需要我们读取路由的值并使用它来加载正确的用户。用于从 API 加载用户的 ID 已经硬编码,点击不同的头像将导致显示相同的用户。
在路由组件中读取路由参数
当一个任务需要你创建一个动态路由时,很可能会需要一个或多个动态参数来正确渲染页面。
如果你正在创建一个博客页面,动态参数将是博客 ID;如果你正在加载一个图书库,参数可能是用于过滤的分类或作者。无论原因如何,使用动态 URL 将需要你在组件中读取和使用该值。
可以使用route包中可用的params对象来实现读取路由参数。让我们回到UserView.vue并修改代码以从路由中动态加载userId:
<script setup>
import { reactive } from 'vue';
import { useRoute } from 'vue-router';
const user = reactive({});
const fetchUser = (userId) => {
const url = `https://dummyapi.io/data/v1/user/${userId}`;
fetch(url, {
"headers": {
"app-id": "657a3106698992f50c0a5885"
}
})
.then( response => response.json())
.then( result => {
Object.assign(user, result.data);
})
}
const route = useRoute();
fetchUser(route.params.userId);
</script>
为了从路由中加载我们的参数,我们对视图进行了三项更改。首先,我们从 vue-router 包中导入了useRoute。然后,我们使用const route = useRoute()创建了一个路由实例,最后,我们移除了硬编码的userId,并用路由参数替换了它,即使用route.params.userId。
使用UseRouter与UseRoute
你可能已经注意到,在之前的组件中,我们使用了useRouter,而在当前组件中,我们使用了useRoute。这两个名字非常相似,它们之间的差异很容易被忽略。useRouter用于我们需要访问路由对象时——例如,添加路由、推送导航和触发路由的前后操作——而useRoute用于获取当前路由的信息,如参数、路径或 URL 查询信息。
在这个阶段,动态路由将完全可用,点击用户头像将加载该特定用户的信息。
图 10.5:显示用户信息的用户视图页面
在本节中,我们学习了动态路由的所有不同方面。我们了解了为什么需要它们以及它们试图解决的问题,并且通过在我们的route数组中添加一个动态路由来介绍它们的语法。在此过程中,我们利用机会学习了如何使用路由的name和params值进行程序化导航,并最终学习了如何使用 vue-router 包提供的路由对象来访问路由信息,如params,从而使我们的用户视图动态化。
在完成本节的同时,我们还回顾了之前学习过的主题,如组件、指令、事件等等。
在下一节中,我们将进一步学习嵌套路由。这将遵循与本节类似的流程,并希望对你来说感觉熟悉。
学习嵌套路由
单页应用(SPAs)非常强大,但添加一个结构良好的路由器,如 vue-router,可以帮助将 SPAs 提升到另一个层次。SPA 的强大之处在于其能够在不刷新页面的情况下动态交换组件的能力,但如果你告诉我 vue-router 甚至可以比页面布局更深层次,你会怎么想?这就是嵌套路由发挥作用的地方。
嵌套路由允许你在路由中定义路由,可以有多层深度,以创建一个非常复杂的布局。嵌套路由的概念可能听起来很复杂,但它们的使用使得应用程序更容易开发,并且对于大多数应用程序来说都是推荐的。
当我们创建主路由时,我们说当进行导航时页面将完全交换;对于嵌套路由,概念相同,但不是交换整个页面,而是只交换页面的一部分内部内容。
图 10.6:嵌套路由示例
为了更好地理解嵌套路由的工作原理,让我们来讨论一下图 10**.6中展示的小例子。想象一下,你已经创建了一个仪表板。在开发仪表板的不同部分,例如settings视图和analytics视图时,你意识到布局中有一部分是共享的。这可能包括导航栏、侧边栏,甚至一些功能,如打印屏幕或聊天。
为了避免重复这些功能,我们可以使用嵌套路由。在我们的例子中,我们将有一个父路由称为dashboard,它将有一组子路由——在我们的例子中,这些将是一个settings嵌套路由和一个analytics嵌套路由。
主要的dashboard路由将包括我们之前提到的所有可复用组件,以及新增的<RouterView>组件。这个组件,就像我们在本章开头app.vue文件中所做的那样,将被路由器用来渲染适当的路由。
在对嵌套路由的理论介绍之后,现在是时候将这个知识应用到我们的 Companion App 中,并学习如何使用 vue-router 的这个新特性了。
将嵌套路由应用于用户
在本节中,我们将回到用户页面,通过添加在用户个人资料和用户帖子视图之间切换的能力来启用嵌套路由。
我们将要开发的例子是一个非常合适的用例,你可能在现实生活中的开发中会遇到。
嵌套路由的想法是视图有共同之处。这可能是仅仅是布局,或者是某些特定的路由参数。一个非常常见的嵌套路由例子是具有标签或提供多步表单的应用程序。
完成这个任务需要几个步骤。首先,我们将创建一个文件,用于加载用户的帖子列表。接下来,我们将重命名userView文件以符合新的路由,最后,我们将修改route数组以定义新的嵌套视图。
对于第一步,我将只提供一些完成任务的建议,因为我希望给你一个机会自己尝试并找出答案。如果你卡住了,可以查看CH10-end分支,其中包含完成的文件。
需求是创建一个文件,该文件将加载用户帖子。这个文件将被命名为userPostsView.vue,并将位于views文件夹中。
这个文件的逻辑将非常类似于SocialPost.vue文件。事实上,两个端点返回的响应是相同的,这使我们能够重用大部分逻辑和 UI。对于本书的范围,我们可以将SocialPost.vue文件的内容复制到我们新创建的文件中,但在实际例子中,我们会重构组件以便重用。
文件需要的唯一修改是从route对象中加载userId,就像我们在userView.vue中所做的那样,然后更改用于加载信息的 URL 为{baseUrl}/user/${userId}/post?limit=10。
测试和开发新组件
你可能会想,“如果你无法在浏览器中测试组件,你如何开发组件呢?”。在开发新组件时,创建用于测试新组件的虚拟路由是一种常见的做法。在这种情况下,我们的路由需要能够从路径中访问用户 ID,因此一个简单的方法是在 route 数组中替换它,以便在访问现有的 user 端点时返回新的组件。
接下来,我们将把 userView.vue 文件重命名为与我们将要进行的路由更改相匹配。新的名称将是 userProfileView.vue。
最后,我们将学习创建嵌套路由所需的语法。因此,让我们打开 router 文件夹内的 index.js 文件,看看分割 user 端点所需进行的更改:
import PrivacyView from '../views/PrivacyView.vue'
import UserProfileView from '../views/UserProfileView.vue'
import UserPostsView from '../views/UserPostsView.vue'
...
{
path: "/user/:userId",
name: "user",
children: [
{
path: "profile",
name: "user-profile",
component: UserView
},
{
path: "posts",
name: "user-posts",
component: UserPostView
}
],
component: UserView
}
嵌套路由所需的语法非常直观,它使用了我们在上一节中获得的知识。
嵌套路由是在我们现有的 user 路由内部定义的,并通过 children 属性来标识。这个属性接受一个数组形式的路由,其结构与我们之前使用的 name、path 和 component 相同。在这里你需要考虑的唯一一点是,当我们定义嵌套路由时,我们不再处于项目的根目录,因此分配给 path 的值会被添加到现有的路径上。所以,在这种情况下,userProfile 路由的完整路径将是 mysite.com/user/:userId/profile。
用户资料和用户帖子视图已经准备好通过导航到 http://localhost:5173/user/:userId/profile 和 http://localhost:5173/user/:userId/posts(其中 userId 被替换为实际的用户 ID)来访问。
嵌套路由并不意味着嵌套布局
你可能已经注意到,在 图 10.6 的例子中,我们提到嵌套路由可以共享布局,并用于更新页面的一部分,例如内部标签页,但在这个例子中,情况并非如此,因为我们使用嵌套路由来完全更新页面。我们所做的是不常见的。事实上,嵌套路由的使用不仅仅是为了共享布局,也是为了共享特定的数据。在我们的例子中,嵌套路由的使用是为了允许两个路由共享名为 userId 的 path 参数。如果你想进一步练习,你可以创建一个用于用户路径的组件,并将其用作用户资料和帖子路由的布局。只需记住,在你的 HTML 中添加一个 <RouterView> 组件来定义 vue-router 应该附加路由的位置。
在本节中,我们学习了嵌套路由的概念。然后,我们将我们的伴侣应用更新,以使用嵌套路由来加载两个不同的用户视图。我们还创建了一个新的视图来显示用户帖子。接下来,我们学习了并应用了实现嵌套路由所需的语法。最终产品是我们能够导航到两个不同的页面,一个显示用户个人资料信息,另一个显示用户帖子。
您可能已经注意到,我们已经将user路由移动到了其子路由之一。因此,如果我们访问user路由(例如,http://localhost:5173/user/60d21b4667d0d8992e610c85),我们将看到一个空页面。这不是一个错误,而是预期的,因为我们实际上没有声明任何满足该特定端点的路由。我们现在将通过创建别名来解决这个问题。
使用别名和重定向重用路由
到目前为止,我们已经学习了如何创建新路由,但掌握 vue-router 还需要另一项技能:别名和重定向。
alias和redirect都允许您通过将用户从一个路由导航到另一个路由来重用现有路由,这在您想要创建 SEO 友好的 URL 时非常有用。这两个功能之间的唯一区别是用户在浏览器中看到的结果。
使用我们之前的示例,我们发现自己处于一个路由目前无法使用的情况,因为它没有组件或视图与之关联。这是在处理children路由时常见的情况,可以通过别名或重定向轻松解决。
为了解决我们的空路由问题,我们将使用redirect将所有到达user/:userId路径的用户导航到其子路径user/:userId/profile。
创建别名或重定向与简单路由相同,只是增加了一个redirect或alias属性,用于指定应使用哪个视图。
让我们更改我们的用户视图以包含redirect属性:
path: "/user/:userId",
name: "user",
redirect : { name: "user-profile" },
children: [
通过简单地添加高亮代码,用户现在将被重定向到另一个路径。如果您尝试访问路径http://localhost:5173/user/60d21bf967d0d8992e610e9b,您将立即看到 URL 变为http://localhost:5173/user/60d21bf967d0d8992e610e9b/profile。
redirect的值可以是包含名称的对象,如我们的示例所示,一个简单的路径,甚至可以是一个包含name、params等更多内容的复杂对象。
现在是时候介绍alias了。之前,我们看到了我们可以使用redirect将用户从一个路由传输到另一个路由。但如果你只想确保用户能够通过多个 URL 访问给定的路由,那会怎么样?一个很好的例子是我们需要创建一个“友好 URL”,这是一个用户容易输入或记住的 URL。对于网站来说,如电子商务网站,在用户浏览网站时定义一个特定的 URL 是很常见的,同时提供一个友好 URL,这通常是 Google 索引和使用的 URL。
仅举一个例子,如果你希望当用户访问mywebsite.com/p/123和mywebsite.com/product/123时渲染相同的路由,我们可以使用别名来实现这一点。为了达到这个目的,我们可以使用别名。
当使用别名时,我们可以定义一个或多个可以与同一路由匹配的 URL。别名接受一个字符串或字符串数组,这些字符串匹配给定路由的不同 URL。
让我们假设,例如,我们希望我们的静态/privacy页面在用户访问/privacy-policy时也能渲染。为了实现这一点,我们将在路由中写下以下规则:
{
path: '/privacy',
name: 'privacy',
alias: '/privacy-policy',
component: PrivacyView,
},
在我们的路由声明中添加别名后,我们的伴侣应用将为localhost:5173/privacy和localhost:5173/privacy-policy两个地址都渲染隐私页面。
在这个小节中,我们介绍了alias和redirect的方法。我们定义了这两个方法解决的问题及其区别。最后,我们在自己的路由定义中实现了这个方法,以防止用户在访问user端点时看到空白页面,并提供了两个端点定义一个路由的简单示例来展示别名。
摘要
在本章中,我们学习了 vue-router,这是书中将要介绍的 Vue 核心库集的第一个外部包。本章通过介绍其配置和设置,如历史模式,向我们介绍了这个包。然后我们学习了路由的使用,如何定义它们,以及如何导航到它们。
在学习完 vue-router 的基础知识后,我们继续深入到更高级的主题,例如动态路由和嵌套路由。
最后,我们学习了alias和redirect,以完成我们对路由及其如何用于构建简单和复杂应用的基本理解。
在下一章中,我们将学习 Vue 生态系统中的另一个核心包:Pinia。这个包用于在应用中定义和共享状态。
第十一章:使用 Pinia 管理您的应用程序状态
构建 Web 应用程序不是一项简单的任务,这不仅是因为编写它们所需的知识的数量,而且还因为成熟的应用程序可能发展的架构复杂性。
当我们最初开始这本书时,我们介绍了简单的话题,比如使用字符串插值替换文本或使用v-if指令隐藏元素。这些功能是 Vue.js 框架的核心,并且是使用此框架构建应用程序所必需的。
随着我们这本书的进展,我们开始介绍那些在项目开发初期并不总是必需的话题——在某些情况下甚至根本不需要。在前一章中,我们介绍了 Vue Router,这是第一个加入 Vue.js 核心框架的附加包。我们将继续这一趋势,通过介绍 Vue 生态系统的一部分核心维护包:Pinia。
Pinia 是 Vue.js 的官方状态管理包。它是之前状态管理包的继承者,该包被称为 Vuex。
为什么有两个不同的名字?
如果这两个包都是由同一个开源维护者创建和维护的,为什么它们有不同的名字?Pinia 原本应该被称为 Vuex 5,但在其开发过程中,他们决定给它一个不同的名字,因为这两个版本在主要方面彼此不同。
正如我之前提到的,并非每个应用程序都需要所有功能,状态管理就是其中之一。实际上,在一个非常小的网站上引入状态管理可能是一个过度架构的迹象。
在本章中,我们将解释什么是状态管理,并介绍 Pinia 作为 Vue.js 生态系统的官方包。然后,我们将讨论应用程序预期包含状态管理系统的情况,并讨论使用一个系统的优缺点。然后,我们将通过在我们的应用程序中包含两个存储来进行一些实践:一个用于处理侧边栏,另一个用于处理我们的帖子。在这样做的时候,我们还将向我们的应用程序添加一些功能,例如从标题栏切换侧边栏的能力以及添加新帖子的选项。
本章有以下部分:
-
何时使用状态管理
-
了解 Pinia 存储的结构
-
使用 Pinia 进行集中式侧边栏状态管理
到本章结束时,你应该熟悉状态管理的概念,并且能够在你未来的应用程序中定义和使用存储。
技术要求
在本章中,我们将使用的分支被称为CH11。要拉取这个分支,请运行以下命令或使用您选择的 GUI 来支持您进行此操作:
git switch CH11
本章的代码文件可以在github.com/PacktPublishing/Vue.js-3-for-Beginners找到。
何时使用状态管理
本章最重要的部分是学习何时在您的应用程序中使用 Pinia,何时不使用。
所有添加到应用程序中的额外包和功能都会带来额外的成本。这种成本体现在学习这些新技能所需的时间、构建新功能可能需要额外的时间、整体架构可能给项目增加的额外复杂性,以及另一个包添加到您的 JavaScript 包中的额外大小。
将状态管理器添加到您的应用程序属于这类可能不是总是需要的可选功能。幸运的是,添加和使用 Pinia 非常简单,并且不会像 React Redux 等其他类似工具那样给项目增加太多开销。
常规做法是,只有在项目足够复杂且包含许多组件层,并且跨应用程序传递值变得复杂时,才应该将状态管理添加到项目中。
Pinia 的一个良好用例是大型应用程序,属性在多个层之间传递。另一个用例是一个 SPA,它具有非常复杂的数据,需要由应用程序的多个部分共享和操作。
状态管理解决的主要问题是属性钻取。因此,应用程序的结构越复杂、越深入,Pinia 就越适合该项目:
图 11.1:属性传递到多个组件层
什么是属性钻取?
从父组件传递数据到子组件的过程,尤其是多层深度的传递,也被称为属性钻取。当谈论状态管理时,这是一个常用的术语,也将在本章的其余部分中使用。
让我们分析几个项目示例,看看我们是否需要 Pinia 的支持来处理状态管理:
-
宣传册网站:一个包含几个静态页面的简单网站,非常适合本地企业。该网站可能包含一个联系表单。没有真正需要管理的数据。
不需要存储
-
个人博客:这个网站稍微复杂一些,具有动态页面,可以渲染我们在 Vue Router 章节中学到的博客页面。数据被传递到页面,但在应用程序的多个部分中不需要修改或使用。
不需要存储
-
电子商务网站:一个用于销售产品的商业网站。该网站将主要是动态的,提供大量的交互性。数据需要通过应用程序的多个层传递和修改,例如从购物车到结账。
需要存储
-
社交媒体网站:一个用于创建和分享帖子的网站。该网站还将有按用户、标签、类别等过滤的页面。相同的数据可以在应用程序的许多部分中重复使用,使用存储可以确保数据只被获取一次,然后重复使用。
需要存储
对于所有网站来说,添加状态管理器并不是必须的,但它是由您特定 SPA 的用例和需求驱动的需求。诚然,随着您职业生涯的进步,您将熟悉框架提供的工具和技术,您会发现您经常过度设计您的应用程序。然而,在您职业生涯的初期,保持简洁并只选择您真正需要的 SPA 工具是非常重要的。
在进入下一节之前,我们应该对使用状态管理的真正好处说几句。我们将通过比较两个具有相同组件结构但处理数据方式不同的应用程序来实现这一点。一个使用属性钻取,而另一个使用状态管理。
我们将从 图 11.1 中所示的示例开始。这个示例类似于我们的伴侣应用,它展示了 post 属性从 App.vue 组件流向屏幕上渲染的最后组件的方式。在这个阶段,应用程序仍然相当简单。即使有一些多层属性钻取发生,复杂性仍然是可接受的。
现在,我们将添加用户编辑帖子的可能性。由于属性可以在定义它们的组件中简单地修改,因此我们不得不在整个组件树中发出一个“编辑”事件:
图 11.2:属性向下传递,事件向上传递到组件树
这里的情况开始变得更加复杂。除了属性钻取之外,我们还有 Post 属性。
我们将包括一个 Pinia 存储,看看它会给桌面带来什么变化。Pinia 将接管 post 属性,并将其提供给需要读取或修改它的组件:
图 11.3:一个 Pinia 存储管理数据
添加存储已经从我们的应用程序中移除了很多复杂性。当我们编辑帖子时,现在唯一需要重新渲染的组件将是 帖子正文。此外,存储使我们能够轻松地向下传递特定数据,例如侧边栏中的 最近帖子。这样做也将确保除非更改的帖子包含在侧边栏中,否则侧边栏不会重新渲染(这也可能使用属性实现,但通常不这样做)。
在这个简短的章节中,我们定义了何时需要状态管理以及何时应该从您的应用程序中省略它。然后,我们通过描述一些示例应用程序来定义在您的应用程序中何时需要存储。最后,我们介绍了状态管理的概念,并涵盖了它为您的应用程序带来的好处。
在下一节中,我们将学习 Pinia 的结构以及我们如何在应用程序中使用它。
了解 Pinia 存储的结构
在本章中,我们将介绍构成 Pinia 存储的内容以及它是如何支持您管理应用程序数据的。Pinia 建立在多个存储的概念之上。每个单独的存储都将管理一组特定的数据或公司逻辑,这些数据或逻辑不绑定到特定的组件。您的应用程序可以有一个帖子存储、评论存储,甚至一个用于管理侧边栏状态的存储。
存储可以相互通信,但最重要的是您应该能够轻松定义单个存储的特点。存储的数据应该很好地分割。
每个存储被分为三个不同的部分:state、getters 和 actions。
这三组在 Pinia 存储中可用的选项实际上可以与我们关于 Vue 单文件组件所了解的现有功能进行比较。
Pinia 中定义的 state 对象与用作私有组件数据的 Ref 或 Reactive 相当。getters 与计算属性相当,因为它们用于创建现有 state 的修改版本。最后,我们有 actions,它们类似于方法,用于在存储上执行副作用。
图 11.4:Pinia 选项与 Vue 组件之间的比较
当我们首次初始化应用程序时,我们选择了 CLI 命令来为我们创建存储。因此,伴侣应用程序在这里提供了一个非常简单的存储示例:src/stores/counter.js。让我们看看它包含的内容,以了解更多关于 Pinia 存储的实际结构:
src/stores/sidebar.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'Eduardo' }),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
存储的第一部分是其声明。存储是通过 pinia 包内可用的 defineStore 方法声明的。在创建存储时,通常期望导出的方法遵循 counter 存储库的格式,我们可以预期导出的方法被命名为 useCounterStore。
接下来,我们有 state 对象。它被声明为一个返回对象的函数。语法可能看起来很熟悉,因为它与我们介绍选项 API 时使用的语法相同,当时我们讨论了使用选项 API 声明 Vue 组件。state 对象包括存储在初始状态下的值。
接下来,我们有获取器(getters),它们是 Vue 组件内部可用的计算属性的等价物。获取器用于使用 state 或其他获取器创建派生值。例如,在一个帖子存储中,我们可能有 visiblePost 获取器,它只返回具有 visible 标志的帖子。获取器接收状态,正如前一个获取器代码片段中显示的第一个参数所示。
最后,我们有动作。这些相当于方法,用于触发一个副作用,可以用来修改一个或多个存储条目。在我们的例子中,动作被命名为 increment,它用于增加 count 状态的值。动作是异步的,可以包括外部副作用,如调用 API 或调用其他存储动作。
现在我们已经学习了存储的结构,是时候学习如何在组件中使用存储了。要在组件中使用存储,我们需要使用由 definedStore 生成的导出方法来初始化它。在计数器存储的实例中,我们的初始化方法将是之前代码块中定义的 useCounterStore:
const counter = useCounterStore();
然后使用它直接访问状态条目:
counter.count
counter.name
同样的方法也适用于获取器和动作:
counter.doubleCount
counter.increment()
在本节中,我们介绍了 Pinia 存储的基本结构。我们学习了如何使用 defineStore 声明它,然后解释了存储的三个不同部分:状态、获取器和动作。最后,我们学习了如何通过使用计数器存储来学习访问其状态、获取器和动作所需的语法。
在下一节中,我们将通过创建一些存储来应用我们迄今为止所学的内容。
使用 Pinia 实现集中式侧边栏状态管理
在上一节中,我们介绍了 Pinia 的基本结构和语法。为了更好地学习和理解状态管理,我们将通过重构一些现有数据到其自己的存储中,来修改我们的伴侣应用。我们将实现两个不同的存储。第一个将是一个非常简单的存储,用于管理侧边栏的状态,而第二个将处理帖子。
处理侧边栏状态的存储将会相当小。这将非常适合我们理解存储的基本语法和用法,而处理帖子的存储将会稍微复杂一些。
状态管理不应用于所有数据,其添加应伴随着支持其使用的好理由。那么,在侧边栏上添加它是正确的吗?
做好你的研究
回到代码库,尝试理解关于侧边栏及其功能的一切。探索性知识在提升你的技术技能方面非常有用。
当前侧边栏提供以下功能:
-
它可以是打开的或关闭的
-
它可以通过按钮切换
-
它使用本地存储来记住其状态
-
所有逻辑都包含在同一个组件中
我们简单的 Sidebar.vue 组件使侧边栏变得既美观又交互。但从前面的列表中,有一行应该引起我们的注意:“所有逻辑都包含在同一个组件中。”
如果你回到上一节,你可能会注意到我们提到使用商店通常与复杂场景相关联,在这些场景中,数据在多个组件之间传递。然而,在我们的情况下,所有数据都存储在一个文件中,逻辑或计算并不多。那么我们为什么需要包含一个商店呢?这样做是个好主意吗?
简短的回答是不。在这种场景下,实际上并不需要商店。即使我在个人项目中可能会用商店来处理侧边栏,我也不建议每个人都应该用所有 Vue 项目这样做。
在当前状态下,侧边栏过于简单,以至于不能将其移动到 Pinia 商店中。
幸运的是,我们对应用程序有完全的控制权,所以我们可以简单地添加一个要求,使使用商店变得合适。在这种情况下,新的要求是从 主标题 添加切换侧边栏的能力。
即使这个要求看起来不合理,侧边栏通常由不同的元素控制是非常常见的。这种情况可能会成为你下一个项目的现实。
为了完成这个新要求,我们需要执行一些属性钻取和事件冒泡,以便在没有商店的情况下使其工作。然而,使用简单的商店,逻辑将被从组件中抽象出来,并且可以很容易地被整个应用程序访问。
创建我们的第一个商店需要两个步骤。首先,我们将通过将现有方法和数据移动到商店中来重构我们的应用程序。其次,我们将更新组件以使用新创建的商店。
正如我们之前提到的,处理侧边栏切换的所有逻辑目前都存储在Sidebar.vue组件中。在这个组件中,我们可以找到以下与侧边栏切换相关的代码:
-
closed状态的声明:const closed = ref(false); -
切换侧边栏的方法:
const toggleSidebar = () => {closed.value = !closed.value;window.localStorage.setItem("sidebar", closed.value);} -
初始化侧边栏状态的生命周期:
onBeforeMount( () => {const sidebarState = window.localStorage.getItem("sidebar");closed.value = sidebarState === "true";});
让我们去创建我们的第一个商店。
创建我们的第一个商店
现在,我们将前面的代码移动到一个名为sidebar.js的新商店中。就像我们之前提到的,Ref变量将变成State,方法将变成 Pinia 动作。
让我们先为我们的商店创建一个空的结构:
src/stores/sidebar.js
import { defineStore } from 'pinia'
export const useSidebarStore = defineStore('sidebar', {
state: () => ({}),
getters: {},
actions: {},
})
我们的空商店包括从pinia包中导入defineStore,使用我们之前提到的命名约定(use + store name + Store)来初始化商店,从而创建useSidebarStore,最后是状态、获取器和动作的三个空选项。
在这个阶段,商店已经准备好填充信息。让我们用侧边栏逻辑来填充它:
src/stores/sidebar.js
import { defineStore } from 'pinia'
export const useSidebarStore = defineStore('sidebar', {
state: () => ({ closed: true }),
getters: {},
actions: {
toggleSidebar() {
this.closed = !this.closed
localStorage.setItem('sidebar', this.closed)
},
loadSidebarFromLocalStorage() {
const closed = localStorage.getItem('sidebar')
this.closed = closed === 'true'
}
},
})
在前面的代码中,我们在状态对象中声明了一个新的值closed。它最初被设置为true。我们目前没有修改getters,因为它将在后面的部分中使用。接下来,我们声明了两个动作。一个是切换侧边栏,另一个是使用我们之前方法中的现有代码从本地存储中加载侧边栏。
如果我们将动作中的代码与组件中存在的方法进行比较,你会注意到它们非常相似。主要区别在于我们可以访问状态的方式。实际上,当这些方法在组件中时,我们必须使用closed.value来访问引用的值,而在 Pinia 中,可以使用this.closed关键字来访问单个状态实体的值。
现在我们已经完成了存储,我们只需要回到侧边栏并替换之前的逻辑为新存储。用存储替换当前逻辑需要三个步骤。首先,我们需要加载和初始化存储。其次,我们需要用 Pinia 动作替换方法,最后,我们需要修改模板以使用存储的状态而不是引用。
让我们先移除之前的引用并初始化存储:
import IconRightArrow from '../icons/IconRightArrow.vue'
import { useSidebarStore } from '../../stores/sidebar';
const currentTime = ref(new Date().toLocaleTimeString());
const router = useRouter();
const closed = ref(false);
const sidebarStore = useSidebarStore();
存储是通过导入并调用useSidebarStore来初始化的。这是我们声明的导出方法。通常声明一个名为store或名称+Store的常量是很常见的。
你知道吗?
使用特定的存储名称,如sidebarStore而不是仅仅调用它为Store,在尝试搜索特定存储的所有用法时非常有用。由于使用状态管理允许你在应用的任何地方使用这种逻辑,所以能够快速搜索它是非常好的,因此有一个一致的命名约定是有帮助的。
在下一步中,我们将处理方法。我们将移除现有方法并用存储动作替换它们:
const toggleSidebar = () => {
closed.value = !closed.value;
window.localStorage.setItem("sidebar", closed.value);
}
const onUpdateTimeClick = () => {
currentTime.value = new Date().toLocaleTimeString();
};
const navigateToPrivacy = (event) => {
event.preventDefault();
console.log("Run a side effect");
router.push("privacy");
}
onBeforeMount ( () => {
const sidebarState = window.localStorage.getItem("sidebar");
closed.value = sidebarState === "true";
sidebarStore.loadSidebarFromLocalStorage();
});
就像之前一样,前面的代码包括两个步骤。它首先移除之前的逻辑,然后用存储实现替换它。我们通过移除处理从状态中检索侧边栏的逻辑,并用loadSidebarFromLocalStorage动作替换它来更新了onBeforeMount生命周期内容。
你可能已经注意到我们还没有替换toggleSidebar方法。这不是一个错误;实际上,我们将能够直接从<template>调用 Pinia 动作。
让我们看看我们的组件 HTML 需要哪些更改来完成我们的重构到 Pinia 存储:
src/components/organisms/sidebar.vue
<template>
<aside :class="{ 'sidebar__closed': sidebarStore.closed}">
<template v-if="sidebarStore.closed">
<IconRightArrow class="sidebar__icon" @click="sidebarStore.toggleSidebar" />
</template>
<template v-else>
<h2>Sidebar</h2>
<IconLeftArrow class="sidebar__icon" @click="sidebarStore.toggleSidebar" />
<TheButton>Create post</TheButton>
更新 HTML 是最容易的改变之一。实际上,这里唯一的必要条件是将所有状态和动作前缀为sidebarStore存储常量。就像动作一样,状态值也可以直接访问,就像sidebarStore.closed所示。这被用来访问closed状态值。
在这个阶段,我们对侧边栏的重构已经完成。所有曾经存在于组件中的逻辑都已经移动到一个新的存储中。侧边栏应该按预期工作,唯一的区别是它的值和逻辑存储在存储中,而不是在组件本身中。
为了完成我们的任务,我们需要允许应用程序的另一部分切换侧边栏。这是我们添加以证明创建存储的必要性和深入了解存储的要求。
将侧边栏扩展到头部
在本节中,我们将进入头部文件,通过添加从应用程序的不同部分切换侧边栏可见性的能力来完成我们的任务。
我们将通过在头部旁边添加一个简单的按钮来实现这一点。就像之前一样,我们将导入并初始化存储,然后直接使用其动作。重要的是要记住,这个新按钮可以放置在应用程序的任何地方,因为它的动作由存储拥有和控制。
让我们进入TheHeader.vue并添加存储:
src/stores/TheHeader.vue
<template>
<header>
<TheLogo />
<h1>Companion app</h1>
<span>
<a href="#">Welcome {{ username }}</a>
<IconSettings class="icon" />
<IconFullScreen class="icon" @click="sidebarStore.toggleSidebar" />
</span>
</header>
</template>
<script setup>
import { ref } from 'vue';
import TheLogo from '../atoms/TheLogo.vue';
import IconSettings from '../icons/IconSettings.vue';
import IconFullScreen from '../icons/IconFullScreen.vue';
import { useSidebarStore } from '../../stores/sidebar';
const username = ref("Zelig880");
const sidebarStore = useSidebarStore();
</script>
我们在头部编写的代码与我们之前在侧边栏中定义的代码之间没有区别。事实上,当使用存储时,我们可以在应用程序的任何地方使用它,而无需定义任何其他内容。所有存储的实例将作为一个整体工作,允许我们从应用程序的不同部分使用和修改状态。
在这个阶段,我们的 Companion App 将增加一个新功能,允许我们通过使用侧边栏本身或从头部来切换侧边栏。
介绍获取器(getters)的概念
在我们进入本章的下一节之前,我们应该介绍存储的另一个特性,该特性已被提及但尚未在我们的 Companion App 中使用:获取器。
获取器与计算属性类似。它们允许你使用存储状态来创建变量。在我们的例子中,我们将引入一个简单的获取器,为我们的侧边栏创建一个友好的open或closed标签。在除这个用例之外,获取器还可以用于翻译目的、过滤数组或规范化数据。
让我们回到sidebar.js文件并添加我们的friendlyState获取器:
src/stores/sidebar.js
state: () => (
{ closed: true }
),
getters: {
friendlyState(state) {
return state.closed ? "closed" : "open";
}
},
actions: {
...
创建获取器非常简单。你必须在获取器对象中声明一个方法,然后添加逻辑来创建获取器将要返回的值。就像计算属性一样,这将被缓存。更重要的是,它不应该产生任何副作用(例如,调用 API 或记录数据)。
关于获取器需要提出的主要观点是它们自动接收状态对象作为第一个参数。因此,要访问关闭状态,我们将编写state.closed。就像计算属性一样,多亏了 Vue 的反应系统,如果状态值发生变化,friendlyState值也会自动更新。
现在我们已经设置了 getter,是时候使用它了。我们回到标题部分,将这个字符串添加到用户设置下方可见的位置。我们可以重用之前导入的存储来访问新定义的 getter:
<template>
<header>
<TheLogo />
<h1>Companion app</h1>
<span>
<a href="#">Welcome {{ username }}</a>
<IconSettings class="icon" />
<IconFullScreen class="icon" @click="sidebarStore.toggleSidebar" />
<p>Sidebar state: {{ sidebarStore.friendlyState }}</p>
</span>
</header>
</template>
<script setup>
import { ref } from 'vue';
import TheLogo from '../atoms/TheLogo.vue';
import IconSettings from '../icons/IconSettings.vue';
import IconFullScreen from '../icons/IconFullScreen.vue';
import { useSidebarStore } from '../../stores/sidebar';
const username = ref("Zelig880");
const sidebarStore = useSidebarStore();
</script>
你可能已经注意到,从之前的代码片段中我们没有需要任何额外的初始化或代码,并且我们能够使用现有的存储来打印friendlyState getters。
现在标题应该会显示我们的字符串:
图 11.5:带有侧边栏状态的伴侣应用程序标题
这是一个简单的例子,帮助我们学习如何重构现有代码,以及如何定义具有状态、getter 和 actions 的存储。最后,它帮助我们学习如何在应用程序中的一个或多个组件中使用存储。
我们将通过在伴侣应用程序中引入另一个存储来继续我们的 Pinia 学习之旅。在下一节中,我们将创建一个将处理我们的帖子的存储。这将比之前的更复杂一些,并允许我们介绍状态管理器提供的更多功能。
使用 Pinia 创建帖子存储
在应用程序中添加状态管理是一个永无止境的任务,因为随着应用程序的增长,它也在不断进化。到目前为止我们在应用程序中所做的工作——通过将逻辑从组件中移出并放入存储中来进行应用程序的重构——是一种常见的做法。正如我们之前提到的,将侧边栏逻辑移入存储中有点过于激进,在真实的应用程序中并不期望这样做,因为逻辑足够小,足以在组件内部运行(即使有属性钻取)。
在本节中,情况有所不同。我们将重构应用程序的一个关键部分到存储中:帖子。处理帖子的数据获取和管理是应用程序的一个关键部分,并且随着应用程序的增长可能会变得更加复杂。正因为如此,将帖子移入存储将改善应用程序的整体结构。
就像之前一样,我们将通过首先分析代码库以找到所有与帖子相关的代码来重构当前代码。然后,我们将创建一个新的存储并将代码移到那里。最后,我们将更新组件以使用存储。
由于这是我们第二次进行这个练习,我将跳过一些步骤,直接进入存储的创建。在你跳到下一节之前,我建议你搜索所有与帖子相关的所有方法,并将它们与我们将在存储中编写的那些方法进行比较。这个练习将非常有价值,因为它将为你提供关于你对应用程序当前理解的洞察。
我们的新存储将被命名为posts.vue。它将保存在src/stores文件夹中,就像我们之前的存储一样。
此存储将包含具有 posts 和 page 属性的状态,以及两个不同的操作:fetchPosts 和 removePosts:
import { defineStore } from 'pinia'
export const usePostsStore = defineStore('posts', {
state: () => (
{ posts: [], page: 0 }
),
actions: {
fetchPosts(newPage = false) {
if(newPage) {
this.page++;
}
const baseUrl = "https://dummyapi.io/data/v1";
fetch(`${baseUrl}/post?limit=5&page=${this.page}`, {
"headers": {
"app-id": "1234567890"
}
})
.then( response => response.json())
.then( result => {
this.posts.push(...result.data);
})
},
removePost(postIndex) {
this.posts.splice(postIndex, 1);
}
},
})
我们通过利用之前引入的命名约定初始化了存储。这产生了一个名为 usePostsStore 的命名导出。然后我们使用空数组为 posts 变量声明了状态,并使用 { posts: [], page: 0 } 的值为 0。接下来,我们复制了我们的方法并将它们转换为 Pinia 的操作。与前存在于组件中的方法以及我们添加到存储中的副本之间的主要区别是我们访问变量(如 page 和 posts)的方式。在 Pinia 存储中,可以通过 page.value 访问状态值,我们将它改为 this.page。
这是我们将要为方法做出的唯一更改,因为其余的逻辑保持不变,并且不需要任何修改。
与上一节相比,我们还没有引入任何新内容,对帖子存储的重构流程与侧边栏存储实现非常相似。将逻辑重构到存储中通常是一个非常简单的练习,我们可以像在两个示例中那样提升和转移大部分逻辑。
现在存储已设置,我们将把注意力转向组件,确保它使用存储状态和操作。在这个过程中,我们将学习如何解构 Pinia 存储以增加组件的可读性。
直接使用如 const { test, test2 } = useTestStore() 这样的语法解构存储是不可能的,因为这会破坏此状态的响应性。
打破响应性意味着如果存储中的值发生变化,该变化将不再传播到组件中。为了解决这个问题,Pinia 提供了一个 storeToRefs 方法,它将允许我们安全地解构 Pinia 存储。
存储或 ref 是个人喜好
使用存储,就像我们在之前的示例中所做的那样,或者将值解构为 Refs 是一个完全个人的选择。没有对错之分。
直接使用存储将明确定义来自存储的数据,因为它将使用存储名称作为所有数据的前缀,例如 myStore.firstName。另一方面,使用 Refs 将生成一个更干净的组件,因为状态不需要前缀,访问状态只需使用 ref 的名称,例如 firstName。
让我们通过将组件更改为使用存储并使用 storeToRefs 方法来完成我们的存储迁移。由于文件包含许多更改,我们将将其分解为多个阶段:
-
初始化存储:
import { usePostsStore } from '../../stores/posts';import { storeToRefs } from 'pinia'const postsStore = usePostsStore(); -
将
private状态替换为store状态:const { posts } = storeToRefs(postsStore); -
将方法替换为存储操作:
const { fetchPosts, removePost } = postsStore; -
更新 HTML。
由于我们选择将存储状态转换为 Refs,因此 HTML 内部不需要进行任何更改,因为旧 Refs 和新 Refs 的名称现在是一致的。我们唯一需要更改的是
watch方法。实际上,因为我们已经将 posts 数组从响应式转换为 ref,现在我们需要添加.value以便它能够正常工作:watch(posts.value,(newValue) => {if( newValue.length <= 3 ) {fetchPosts(true);}})
在我们继续之前,我想将您的注意力引到第 3 步。实际上,如果您足够细心,可能会注意到我们没有使用storeToRefs直接提取了存储的动作。这是可能的,因为动作是无状态的,并且没有任何响应性。
完整的文件将看起来像这样:
<template>
<SocialPost
v-for="(post, index) in posts"
:username="post.owner.firstName"
:id="post.id"
:avatarSrc="post.image"
:post="post.text"
:likes="post.likes"
:key="post.id"
@delete="removePost(index)"
></SocialPost>
</template>
<script setup>
import { watch } from 'vue';
import SocialPost from '../molecules/SocialPost.vue'
import { usePostsStore } from '../../stores/posts';
import { storeToRefs } from 'pinia'
const postsStore = usePostsStore();
const { posts } = storeToRefs(postsStore);
const { fetchPosts, removePost } = postsStore;
watch(
posts.value,
(newValue) => {
if( newValue.length <= 3 ) {
fetchPosts(true);
}
}
)
fetchPosts();
</script>
实现添加帖子动作
在几章之前,我们介绍了一个旨在向我们的状态添加新帖子的组件。这个组件从未完全实现,因为它缺少创建帖子所需的主要逻辑。组件被留下这种状态的原因是,在没有存储的情况下添加功能将需要大量的属性钻取和事件冒泡。
多亏了帖子存储,这种情况不再存在。实际上,我们将能够使用存储动作生成添加新帖子所需的逻辑。
存储是帖子的唯一所有者,所以我们不必担心帖子在哪里被使用。我们可以简单地创建一个添加帖子的动作,知道存储将处理应用程序其余部分的状态传播和处理。
首先,我们将在posts.js文件中添加一个动作:
src/stores/post.js
addPost(postText) {
const post = generatePostStructure(postText);
this.posts.unshift(post);
}
addPost动作将通过在帖子列表的开头添加帖子来添加帖子。对于我们的 Companion App 的范围,添加新帖子将只设置帖子的主要内容,因为其他信息,如 ID 和用户信息,将被硬编码并由名为generatePostStructure的函数提供。
接下来,我们将初始化存储并将此动作附加到createPostHandler:
<script setup>
import TheButton from '../atoms/TheButton.vue';
import { usePostsStore } from '../../stores/posts';
import { onMounted, ref } from 'vue';
const postsStore = usePostsStore();
const { addPost } = postsStore;
const textareaRef = ref(null);
const createPostForm = ref(null);
const createPostHandler = (event) => {
event.preventDefault();
if(createPostForm.value.reportValidity()){
addPost(textareaRef.value.value);
};
}
...
使用新动作遵循之前使用的相同语法。首先,我们导入存储。其次,我们初始化它。然后,我们解构我们想要使用的动作,最后,我们像使用简单方法一样使用动作。
之前的例子使用了文本区域 ref 来获取textarea的值。这不是 Vue.js 的正确用法,并且应该避免以这种方式访问值。实际上,在下一章中,我们将通过引入v-model的双向绑定对这个文件进行重构。
创建自己的存储
在进入下一章之前,您应该尝试创建自己的存储。您可以通过创建一个可以处理create post组件可见性的存储来创建一个与侧边栏非常相似的存储。您可以使用侧边栏中标记为CreatePost.vue可见性的按钮。您可以在CH11-END分支中看到完整的实现。
在本节中,我们继续通过重构帖子数据来学习关于 Pinia 存储库的知识。我们通过定义其状态和动作创建了一个新的存储库。然后,我们将现有代码转换为在 Pinia 存储库中工作。接下来,我们介绍了 storeToRefs 方法,并学习了如何从存储库中解构状态和动作。最后,我们通过添加用户通过创建新的动作添加新帖子的能力来利用新的存储库。您在这里学到的不是 Pinia 提供的所有功能的完整列表,而是一个对优秀状态管理包的快速介绍。随着您练习的增多,您将了解其他功能,如 $patch 和 $reset。
摘要
将状态管理引入您的应用程序可以真正帮助您轻松处理数据。在本章中我们分享的两个示例中,我们看到了状态管理如何通过避免属性钻取和事件冒泡来为您的应用程序增加好处。
在我们结束本章之前,我想分享使用状态管理在您的应用程序中的一个额外好处。在前两节中我们所完成的重构突出了使用 Pinia 存储库帮助我们从组件中移除大量逻辑到存储文件的单个位置这一事实。
这种抽象不仅对开发体验有益,还可以用来选择我们应用程序中哪些部分可以进行单元测试。您可能还记得从第八章,选择要测试的内容相当复杂,因为测试过多和测试过少之间有一条非常细的界限。我个人使用状态管理来界定我将要单元测试的应用程序部分。我通过始终确保所有存储库都得到彻底测试来实现这一点。
在本章中,我们首先介绍了状态管理的概念,并讨论了 Pinia 提供的语法和功能。然后,我们开始将所学知识付诸实践,通过将侧边栏重构为其自己的存储库。通过这样做,我们学习了如何声明状态、获取器和动作,以及如何在组件中使用它们。接下来,我们继续学习,通过重构我们应用程序的另一部分:帖子。我们创建了一个存储库,并将方法转换为 Pinia 动作。最后,我们学习了如何解构状态以及状态管理在我们应用程序架构中的重要性。
在下一章中,我们将学习如何通过引入 v-model 的双向绑定和客户端验证的 VeeValidate 来处理我们应用程序中的表单。
第十二章:使用 VeeValidate 实现客户端验证
一个网站在拥有某种形式的表单之前是不完整的。联系我们、反馈和评论表单只是我们可能需要在 Vue.js 应用程序中开发和验证的几个例子。
在本章中,我们将解释什么构成了语义正确的表单,介绍可用的字段,并讨论其无障碍需求。然后,我们将更新我们的 CreatePost 组件,学习如何处理表单字段和管理使用 v-model 的双向绑定。接下来,我们将继续创建一个新的表单,称为 contactUs。这将用于介绍一个新的包,称为 VeeValidate。最后,我们将学习如何开发自定义验证规则,以及如何通过使用 VeeValidate 预设规则简化我们的验证。
在本章中,我们将涵盖以下内容:
-
理解表单
-
使用
v-model进行双向绑定 -
使用 VeeValidate 控制你的表单
-
使用 VeeValidate 定义你的表单验证
到本章结束时,你应该对表单及其在 Vue.js 中的处理有了很好的理解。你将能够定义结构良好且无障碍的表单,使用 v-model 处理用户输入,并使用 VeeValidate 定义具有验证的复杂表单。
技术要求
在本章中,分支被命名为 CH12。要拉取此分支,请运行以下命令或使用你选择的 GUI 来支持此操作:
git switch CH12
本章的代码文件可以在 github.com/PacktPublishing/Vue.js-3-for-Beginners 找到。
理解表单
无论你是开发新手还是有经验,花点时间了解什么构成了一个好的表单以及如何最佳地定义表单都是很重要的。
我在 10 年前开始学习表单,但即便如此,在我完成本章研究的过程中,我仍然发现了一些新的东西可以学习。HTML5 的增强和无障碍要求已经改变了我们定义表单的方式。在本节中,我们将了解什么构成了一个好的表单,然后在后面的章节中,我们将利用这些知识在我们的伴侣应用中定义一些表单。
在大多数静态网站中,例如宣传册网站或博客,表单是用户与你的网站互动的唯一时刻,因此它们需要提供出色的用户体验(UX)并尽可能的无障碍。
一个好的表单包括三个不同的方面:
-
它具有语义正确性
-
它是无障碍的
-
它经过了验证
到本章结束时,我们将涵盖所有三个方面,你将能够创建出色的表单。
让我们从第一个要点开始,看看什么构成了一个好的 HTML 结构。使用正确的 HTML 不仅会提升我们的用户体验,也会为稍后本节中将要讨论的无障碍工作打下基础。
请注意,这本书不是关于基本的 HTML 和 JavaScript,而是专注于 Vue.js。因此,本节将只是一个非常快速的对表单的介绍;我们不会花太多时间深入细节,但会涵盖主要主题,这样你可以轻松地跟随本章的其余部分,并确保你的表单结构良好。
将你的表单包裹在元素中
让我们从经常被忽视的基本知识开始。所有表单都需要被包裹在<form>元素中。这不仅仅是一件好事,它背后还有一些非常重要的意义。
使用<form>元素有三个好处。首先,它通知浏览器存在表单(对浏览器扩展很重要)。其次,它通过支持表单模式(屏幕阅读器用于在网站上完成表单的特定模式)来提高使用屏幕阅读器的视觉障碍用户的用户体验。接下来,它有助于处理验证。
你可能想知道使用<form>元素如何提高用户体验。好吧,你有没有想过为什么有时可以通过按Enter键提交表单,而有时却不能?好吧,这是由<form>元素驱动的。当一个表单被包裹在<form>元素中时,如果用户在完成表单时按下Enter键,表单将触发其submit方法。
不要忘记标签
<label>元素的主题在我心中占有特殊的位置。许多开发者和设计师忽略了这一元素的重要性,要么将其从 UI 中移除,要么误用它。几年前,有一种趋势是开发紧凑的表单设计,其中占位符取代了标签。这些表单看起来非常清晰,开始被到处使用,但从用户体验的角度来看,它们是一个巨大的失败。即使它们看起来很棒,它们也可能很难使用,因为占位符在用户(或浏览器)在字段中输入内容后就会消失,并且无法再次看到,直到用户删除输入。
标签的另一个问题是它们在可访问性方面是必不可少的。事实上,没有它们,视觉障碍用户将无法知道字段的内容,因此将无法完全填写表单。
标签可以用两种不同的方式使用。它既可以作为一个独立的元素使用,使用 ID 将其链接到输入字段,也可以将其包裹在它所属的输入字段中:
// Label standalone
<label for="name">Name:</label>
<input type="text" name="name" id="name">
// label wrapping input
<label>Email:
<input type="email" name="email" id="email">
</label>
这两种方法在语义上都是正确的,它们的使用通常是由你可能需要实现的设计驱动的。
不仅仅是 type="text"
正如我之前提到的,我很早就开始使用表单了,随着时间的推移,有一件事是进步的,那就是现在所有主流浏览器都支持的输入类型的不同。
这是所有可用输入类型的列表:
-
按钮 -
复选框 -
颜色 -
日期 -
日期时间 -
电子邮件 -
文件 -
隐藏 -
图片 -
月份 -
数字 -
密码 -
单选按钮 -
range -
reset -
search -
submit -
tel -
text -
time -
url -
week
浏览器提供了电话号码、日期甚至电子邮件的字段。但使用它们的益处是什么?为什么不用一个简单的text字段呢?好吧,区别可能不在于字段的外观,而在于字段的工作方式。
不同的字段可能会触发不同的 UI。例如,点击日期字段会打开日历,数字字段可能会显示小开关,等等。这种 UX 的改进在手机上尤为明显,因为手机可以提供定义良好的原生组件来支持用户完成表单。
设置表单以自动填充
谁不喜欢一个完全由浏览器自动填充的表单呢?幸运的是,浏览器正在非常努力地工作,以便尽可能多地自动填充表单。我们可以做几件事情来帮助浏览器。
首先,我们需要通知浏览器我们希望表单能够自动填充。这不是自动完成的,而是需要<form>元素接收一个autocomplete属性:
<form autocomplete >...</form>
接下来是确保我们用正确的名称描述字段,因为浏览器可以读取名称并分配正确的字段。所以,如果一个字段是地址,你应该给它正确的address名称,而不是使用像addr或其他浏览器无法理解的缩写。
最后但同样重要的是,定义正确的值来分配给自动完成属性。在主表单上设置autocomplete可能并不足够。有时,例如在用于自动填充密码的浏览器扩展程序的情况下,浏览器需要更多信息,这可以通过在字段上直接使用autocomplete属性来传递。autocomplete可以接受不同的值,具体取决于输入的角色。以下是一些示例:
-
对于用户名:
<input type="email" autocomplete="username" id="email" /> -
对于注册表单:
<input type="password" autocomplete="new-password" id="new-password" /> -
对于登录:
<input type="password" autocomplete="current-password" id="current-password" />
如用户名示例所示,输入的类型和名称不必与它们的autocomplete值匹配。实际上,在用户名的情况下,即使浏览器看到并使用该字段作为电子邮件,密码工具也会用用户名填充该字段。
在本节中,我们学习了如何定义一个好的表单。将表单包裹在<form>中,设置autocomplete,为每个字段定义正确的标签,以及使用正确的输入类型,这些都是我们可以采取的几个步骤,以确保我们的用户在填写我们的表单时拥有良好的体验。
在下一节中,我们将学习如何定义和使用 Vue.js 中的表单,以及框架如何帮助我们进一步改进 UX。
使用 v-model 的双向绑定
在前一个部分,我们学习了我们的表单应该如何定义,现在是时候通过在我们的伴侣应用中添加几个表单来将我们的学习付诸实践了。在本节中,我们将学习如何使用v-model使我们的输入字段实现双向绑定,这是一个术语,用来描述当字段可以同时发出更改事件和更新值时(因此是双向的)。
在前一章中,我们提到用来在CreatePost组件中管理值的解决方案是次优的,现在是时候将其调整为使用最佳行业标准了。
让我们先重新阐述双向绑定真正实现的内容。让我们以一个输入字段为例。到目前为止,我们已经学习了如何使用ref设置该字段的值。ref的使用允许我们在加载时设置特定输入字段的值,但当我们想要通过访客的输入来设置值时,这并不很有用。在前一章中,我们学习了如何使用事件来发出更改,因此,在输入字段的案例中,我们可以发出一个更改事件,该事件会改变Ref变量,进而更新字段的值。
这看起来可能像这样:
<input value="firstName" @input="firstName= $event.target.value" />
即使前面的代码确实实现了双向绑定,它看起来并不整洁,而且在我们所有的表单中添加这些代码会使 HTML 非常难以阅读。
幸运的是,Vue.js 框架有一个很好的快捷方式可以使我们的 HTML 更简洁,称为v-model。
将前面的代码改为使用v-model将导致以下语法:
<input v-model="firstName" />
幕后,v-model和event/value语法生成相同的代码,但由于v-model的代码简洁,当可能时,它是建议的选项来实现双向绑定(可能存在我们需要手动处理事件以处理特定副作用的情况;在这些场景中,event/value语法可能更受欢迎)。
现在我们已经了解了语法以及如何定义表单,让我们回到CreatePost组件并应用我们的学习:
<template>
<form
v-show="postsStore.inProgress"
ref="createPostForm"
@submit.prevent="createPostHandler"
>
<h2>Create a Post</h2>
<label for="post">Enter your post body:</label>
<textarea
rows="4"
cols="20"
ref="textareaRef"
required="true"
minlength="10"
v-model="postText"
name="post"
id="post"
></textarea>
<TheButton>Create Post</TheButton>
</form>
</template>
<script setup>
import TheButton from '../atoms/TheButton.vue';
import { usePostsStore } from '../../stores/posts';
import { onMounted, ref } from 'vue';
const postsStore = usePostsStore();
const { addPost } = postsStore;
const textareaRef = ref(null);
const createPostForm = ref(null);
const postText = ref("");
const createPostHandler = (event) => {
event.preventDefault();
if(createPostForm.value.reportValidity()){
addPost(textareaRef.value.value);
};
addPost(postText.value);
}
onMounted( () => {
textareaRef.value.focus();
});
</script>
让我们分解我们对表单所做的所有更改。首先,我们移除了createPostForm的Ref初始化。这个引用是为了保存 HTML 表单的值,但在重构之后不再需要。接下来,我们创建了一个名为postText的引用,它将保存我们输入字段的值。然后我们在输入字段中添加了v-model,将其分配给新创建的postText。
在首次加载时,输入字段将是一个空字符串(const postText = ref("");),但这个值将由v-model在用户输入到输入字段时自动更新。幕后,v-model在首次加载时分配值,然后每次输入字段发出更改事件时自动更新它。
在重构表单时,我们还通过向@submit事件添加.prevent修饰符引入了一个小的增强。这样做会在提交表单时自动调用event.preventDefault()。
最后的更改是在addPost方法的负载中。现在该方法使用postText.value而不是使用输入引用来获取值。
快速提醒
提醒一下,使用模板引用(Template Ref)来获取输入值仅用于教学目的,不应在实际生活中使用。使用模板引用(Ref)来访问 HTML 对象仅应用于无法通过基本 Vue.js 功能(如指令、计算属性和事件)实现的方法和动作。
在这个阶段,表单完全使用 Vue.js 支持的双向绑定和v-model功能正常工作。如果我们尝试使用表单,这将按预期工作,要么创建一个帖子,要么在输入错误时显示浏览器原生的验证,如图图 12**.1所示。
图 12.1:在创建帖子表单中显示的原生验证消息
通过在本节中获得的知识,你将能够创建表单并在你的应用程序中处理用户输入。在许多情况下,原生浏览器的验证要么不够“美观”,或者不能满足表单的验证要求。
因此,在下一节中,我们将学习如何使用名为 VeeValidate 的外部包来控制我们表单的验证。
使用 VeeValidate 控制你的表单
在上一节中,我们学习了如何使用v-model处理用户输入,如表单字段,但在现实生活中,处理表单比仅仅设置双向绑定要复杂得多。
实际上,一个完整的表单将包括复杂的验证和错误处理,仅举两个例子。即使我们能够手动使用 Vue.js 实现这些功能,我们也可能花费大量时间和精力在由外部包很好地处理的事情上。因此,在本节中,我们将介绍VeeValidate,这是一个旨在使表单开发变得简单的包。
在官方网站(vee-validate.logaretm.com/)上,VeeValidate 被描述如下:
“VeeValidate 是最受欢迎的 Vue.js 表单库。它负责值跟踪、验证、错误、提交等。”
并非每个你将要编写的表单都需要使用外部包,但如果你想创建一个高质量的表单,这个包会为你提供一个一致的方式来定义和处理逻辑。
处理 Node 包的第一步是安装它。要安装 VeeValidate,我们需要打开项目根目录下的终端并运行以下命令:
npm install vee-validate
此命令将包添加到我们的存储库中,以便我们可以导入和使用。
在下一步中,我们将创建一个表单,并学习如何使用这个包使表单开发变得简单且一致。我们本可以使用“创建帖子”表单,但我们将创建一个新的,这样你就可以在将来的代码库中查看两个示例。
这个新表单的一些基本框架已经就位。我们在侧边栏上有一个新的按钮,标签为router对象,还有一个名为ContactView.vue的 Vue.js 组件,它使用我们在前一章中定义的静态模板。最后但同样重要的是,我们还有一个基本的页面框架,其中包含一个名为ContactUs的组件,它将成为我们创建这个新表单的游乐场。
为了更好地理解 VeeValidate,我们将分步骤构建我们的表单,从基本的表单创建开始:
<template>
<form @submit="handleSubmit">
<label for="email">Email</label>
<label for="message">Message</label>
<TheButton>Send</TheButton>
</form>
</template>
<script setup>
import TheButton from '../atoms/TheButton.vue';
const handleSubmit = ({email, message}) => {
console.log("email:",email)
console.log("message:",message);
};
</script>
我们创建了一个带有submit事件绑定到名为handleSubmit的方法的<form>元素。然后我们定义了两个标签,一个用于email,另一个用于message,最后是一个用于提交表单的按钮。
你可能已经注意到我们还没有任何输入字段。这些是有意被省略的,因为它们将使用 VeeValidate 提供的内置组件。
使用 VeeValidate 将完全控制表单,包括其状态,这意味着我们不需要像在之前的例子中使用v-model那样定义或管理单个值。
VeeValidate 能够实现这一点的途径是使用专门构建来为我们处理和管理表单的内置组件。
在本节中,我们将查看三个组件:Form、Field和ErrorMessage。
Form组件将取代 HTML 中可用的原生<form>元素。Field将取代<input>字段,最后,ErrorMessage将用于在字段进入error状态时显示自定义消息。
让我们回到我们的表单,并更新它以使用新的内置组件,然后讨论它们是如何被使用的:
<fForm @submit="handleSubmit">
<label for="email">Email</label>
<Field
id="email"
type="email"
name="email"
></Field>
<label for="message">Message</label>
<Field
id="message"
as="textarea"
name="message"
></Field>
<TheButton>Send</TheButton>
</fForm>
</template>
<script setup>
...
我们现在已将表单更新为使用 VeeValidate。第一个变化可能不太明显,但它包括将<form>元素更改为 VeeValidate 自定义元素<Form>。自定义<Form>元素由 VeeValidate 用于处理表单值的状态。
接下来,我们为每个标签配对了一个Field组件。这个组件是从vee-validate包中导入的。
Field组件与原生的表单元素(如<input>和<radio>)非常相似,并接受大多数你会在原生输入中使用的属性,例如占位符和名称类型。
我们创建了两个字段。第一个字段用于电子邮件,有三个属性:id、type(它是email)和name,它将被用来将字段与其错误消息连接起来。
第二个字段与message相关联;它与用于电子邮件的前一个字段类似,唯一的区别是有一个额外的属性称为as。这个属性用于指定字段应该渲染为哪个表单元素。当使用不带as属性的Field组件时,它默认为<input>元素。实际上,对于我们的消息,我们希望字段是一个文本区域,我们通过使用as属性并将其赋值为textarea来实现这一点。
在这个阶段,表单已经有了使其功能所需的 HTML 和逻辑。如果我们打开我们的应用程序在http://localhost:5173/contact并填写表单,我们将在控制台看到表单正在提交:
图 12.2:由“联系我们”表单触发的控制台消息
即使我们对 VeeValidate 还没有完成,你也应该能够看到它带来的好处。你可能已经意识到,我们从未需要为电子邮件和消息声明任何引用或使用v-model定义任何双向绑定。所有这些都在后台由带有Form和Field自定义字段的包处理。
在本节中,我们已经将 VeeValidate 添加到我们的表单中,并学习了这将给我们的应用程序带来哪些好处。然后我们重构了我们的Field和Form组件。
在我们完成本章之前,我们只需要对我们的表单进行最后一次修改:验证。前面的例子显示,我能够提交一个包含错误值(如伪造的电子邮件和非常简短的消息)的表单。在下一节中,我们将学习如何使用 VeeValidate 进行表单验证。
使用 VeeValidate 定义您的表单验证
一个表单只有在某种形式的验证之后才算完整。表单的值会发送到我们的服务器,我们希望确保我们的表单可以在任何字段值不正确时为用户提供即时反馈。
在我们的案例中,我们在图 12.2中使用的表单将无法通过后端验证,因为电子邮件格式不正确。在本节中,我们将学习如何创建自定义验证,并介绍 VeeValidate 提供的另一个支持包,该包包含一组预置的验证规则,以加快我们的开发速度。
前端验证是不够的
记住,像 VeeValidate 执行的前端验证只能提高用户体验,但它的安全性不足,因为它很容易被绕过。当与表单一起工作时,您还应该在后端定义验证。使用同时在前端和后端工作的验证模式,例如 Zod,可以帮助。
VeeValidate 提供了使用声明式方法或使用组合函数(VeeValidate 提供的一组可组合函数,可用于创建自己的表单组件)来定义其规则的可能性。对于本书的范围,我们将使用声明式方法。
为了验证我们的输入字段,我们需要定义一些验证规则。这些规则将是运行在单个或多个输入字段上的特定条件的方法。一个例子可能是一个required验证,它会检查输入值是否已定义,或者一个最小字符数规则,它会检查字符数是否等于或大于设置的极限。
我们将要定义的第一个规则是一个简单的required规则。为了定义一个规则,我们使用一个名为defineRule的 VeeValidate 方法。此方法接受两个参数。第一个是规则名称,第二个是执行验证的函数。我们可以在ContactUs.vue文件中添加以下代码:
<script setup>
import { Field, Form, ErrorMessage, defineRule } from 'vee-validate';
import TheButton from '../atoms/TheButton.vue';
defineRule('required', value => {
if (!value || !value.length) {
return 'This field is required';
}
return true;
});
声明一个规则验证与在 Vue 中本地定义的方法没有区别。在我们的required规则的情况下,我们使用!value和value.length分别检查值是否设置以及它是否至少有一个字符。然后,我们返回true(如果验证成功通过)或者返回一个字符串作为错误信息。
在声明规则后,剩下的就是在表单字段中使用它。验证规则可以分配给之前使用的<Field>元素。Field组件期望一个名为rules的属性。此属性可以接受一个字符串或一个对象。我更喜欢字符串表示法,因为它类似于 HTML 的本地验证,而且因为它有更短的语法,有助于保持<template>部分清晰易读。
将required规则应用于我们的email字段将生成以下代码:
<Field
type="email"
name="email"
placeholder="Enter your email"
rules="required"
></Field>
在这个阶段,除非验证成功通过,否则我们的表单将不再提交。实际上,如果表单中的所有字段都有效,VeeValidate 将仅触发我们的submit方法。下一步需要做的是在表单无效时向用户显示错误信息。这可以通过 VeeValidate 提供的另一个组件实现,称为ErrorMessage。错误信息本身已经在我们的验证规则中定义,VeeValidate 将负责显示和隐藏信息的逻辑。
ErrorMessage组件接受一个名为name的属性,用于将其与特定的输入字段连接。因此,在我们的情况下,它将是name="email":
<Label for="email">Email</Label>
<Field
type="email"
name="email"
placeholder="Enter your email"
rules="required"
></Field>
<ErrorMessage name="email" />
当尝试提交表单时,现在我们将看到一个错误信息,如图图 12**.3所示。
图 12.3:显示验证错误的表单
即使我们的错误信息显示成功,它也没有真正突出,因为它使用了与页面上的其余文本相同的颜色。这是因为 VeeValidate 提供的 ErrorMessage 组件只是一个实用组件,这意味着它实际上并不提供任何 HTML 标记,但它用于提供一些额外的功能,例如处理错误位置和可见性。
为了改进我们的 UI,我们可以使用 as 属性来定义一个将用于包装错误信息的 HTML 元素,就像我们处理 textarea 一样,我们可以使用 "as" 属性来定义组件应该渲染为哪个 HTML 字段,并添加一些类。代码库已经为名为 error 的类提供了一些样式。所以,让我们去修改我们的 ErrorMessage 组件以使用这个类:
<ErrorMessage as="span" name="email" class="error" />
在添加 as 属性和 class 属性后,我们的组件现在将在屏幕上更加突出。
图 12.4:显示红色错误信息的表单
表单开始成形。表单字段和标签已设置,submit 处理器已就位(即使只是一个占位符),验证规则已定义并正在工作。
在本章的下一节和最后一节中,我们将了解 VeeValidate 提供的预设验证规则。声明一个简单的规则,如 required,很简单,但情况并不总是如此。
我个人非常喜欢使用预设规则,因为它们帮助我保持组件小巧且易于阅读,同时仍然能够提供复杂的验证规则。
使用 VeeValidate 规则
到目前为止,我们只是对 email 字段应用了一个简单的验证,检查它是否已设置,但即使字段目前只需一个字符就能通过验证,这仍然会导致问题。
在本节中,我们将验证电子邮件(以确保其格式有效)和消息文本区域(以确保我们收到的消息至少有 100 个字符)。
我们不是通过定义其规则来手动声明验证,而是将使用 @vee-validate/rules 包,并使用其两个预设规则来实现我们的验证需求。
VeeValidate 提供了超过 25 条规则(vee-validate.logaretm.com/v4/guide/global-validators#vee-validaterules),包括简单的规则,如 required、numeric 和 emails,以及复杂的规则,如 one_of、not_one_of 和 regex。
在学习使用这些规则所需的语法之前,我们需要在我们的应用程序中安装该包。我们可以通过运行以下命令来完成此操作:
npm install @vee-validate/rules --save
VeeValidate 和其验证的强大之处在于,它允许我们对单个字段使用多个验证,允许我们定义复杂的规则,例如 password 字段定义的规则,而无需编写任何代码。
为了实现我们的验证需求,我们将使用email和min规则:
<template>
<Form @submit="handleSubmit">
<Label for="email">Email</Label>
<Field
type="email"
name="email"
placeholder="Enter your email"
rules="required|email"
></Field>
<ErrorMessage as="span" name="email" class="error" />
<label for="message">Message</label>
<Field
as="textarea"
name="message"
rules="required|min:100"
></Field>
<ErrorMessage as="span" name="message" class="error" />
<TheButton>Send</TheButton>
</Form>
</template>
<script setup>
import { Field, Form, ErrorMessage, defineRule } from 'vee-validate';
import TheButton from '../atoms/TheButton.vue';
import { required, email, min } from '@vee-validate/rules';
defineRule('required', required);
defineRule('email', email);
defineRule('min', min);
...
让我们分析我们最新的代码片段,看看如何使用 VeeValidate 的验证规则。
首先,我们手动从@vee-validate/rules包中导入验证。我们本来可以全局导入所有规则,但我更喜欢在每个组件内部进行。这不仅帮助我理解组件范围内使用的验证,还能通过确保我们只导入代码库中使用的规则来保持构建大小小。
然后,我们移除了之前定义的required规则,并用 VeeValidate 提供的规则替换了它。我们声明了两个新规则,分别称为email和min。我们使用从@vee-validate/rules导入的规则通过defineRule方法做到了这一点。
接下来,我们在组件的<template>部分添加了我们的验证。rules属性可以接受由字符|分隔的多个条目。在email的情况下,规则是"required|email",但对于textarea,它们是"required|min:100"。
最后,我们在消息文本区域添加了ErrorMessage。
你可能已经注意到,message字段中使用的规则与email中使用的规则略有不同。这是因为min规则需要一个等于字段在标记为有效之前所需字符数的参数。为了为规则设置指定的参数,我们添加一个冒号(:)后跟一个或多个用逗号分隔的值。例如,为了定义一个max规则为164个字符,我们会写rules="max:164",而为了定义一个允许在10到20之间的数字的规则,我们会写rules="between:10,20"。
规则可以被定义为对象
如我之前提到的,rules属性可以被定义为对象。你的选择是个人喜好,没有对错之分。如果我们想将message字段规则替换为对象,我们会写rules="{ required: true, min: 100}"。
在本节中,我们学习了如何验证我们的表单。我们首先通过创建一个简单的required验证来定义和使用验证规则,然后介绍了预设的验证规则,并使用它们来完全验证我们的表单。
你的回合
继续工作在handleSubmit方法上,以发送一个fetch请求(到一个假端点)并带上表单输入。
概述
在本章中,我们通过引入使用v-model的双向绑定和 VeeValidate 进行表单验证及处理来学习如何处理用户交互。在章节中,我们重新定义了语义正确表单的构成要素,学习了如何使用v-model语法来定义双向绑定,然后通过引入 VeeValidate 进入表单领域,并看到了它如何被用来定义、处理和验证我们的表单。
在下一章中,我们将从编码中退一步,通过介绍调试技术和使用精美的 Vue 调试器浏览器扩展来学习如何调查和解决问题。
第四部分:结论和进一步资源
在本书的最后一部分,我们将探索阅读材料、资源和主题,这些将使你对 Vue.js 框架的知识更进一步。在这一部分,我们还将学习如何调试我们的应用程序。
本部分包含以下章节:
-
第十三章*,使用 Vue Devtools 揭示应用问题*
-
第十四章*,未来阅读的高级资源*
第十三章:使用 Vue Devtools 揭示应用程序问题
如果说 Vue.js 在其他框架和库中有一个明显的优势,那就是其对 开发体验(DX)的关注。从开始,Vue.js 就专注于为开发者提供良好的体验,并且这种体验在几年前随着 Guillaume Chau 创建 Vue Devtools 达到了顶峰。
可作为 Firefox 和 Chrome 扩展程序或独立 Electron 应用程序提供的 Vue Devtools 一直是 Vue.js DX 的核心。
最近,Vue Devtools 因其能够为 Nuxt.js(Vue.js 的元框架)提供深入了解而备受关注,帮助开发者通过简单直观的界面理解全栈 JavaScript 应用程序的复杂性。
我们将从这个章节开始,学习如何在我们的首选浏览器中安装和使用扩展程序;然后我们将通过理解扩展程序的每一部分如何工作以及它们如何相互配合来了解其布局。然后我们将检查每个单独的部分,组件、时间轴、Pinia 和 vue-router,以向您提供对扩展程序的全面理解。
本章包括以下主题:
-
熟悉 Vue Devtools
-
深入探索 Vue Devtools 时间轴标签
-
使用 Vue Devtools 插件分析附加数据
到本章结束时,您将很好地理解 Vue Devtools,并能够在日常生活中使用它。您将能够使用它来创建新组件,检查您的应用程序组件树,在时间轴中记录用户交互,并最终利用 Pinia 和 vue-router 等包信息。
熟悉 Vue Devtools
即使是最资深的开发者也依赖于调试工具来帮助他们开发高质量且无错误的代码(没有代码是完全无错误的,但这是开发时的目标)。Vue Devtools 的目标是提供对 Vue.js 框架不同部分的快速洞察,这可以帮助我们完成日常任务。
我们可以通过在我们的代码中放置 alert 和 console.log 或其他首选方法来调试我们的应用程序,但如果您可以直接在浏览器中使用一个非常漂亮且干净的界面找到所有所需信息呢?这就是 Vue Devtools。
在本节中,我们将学习如何在我们的浏览器上启用 Vue Devtools,并了解此扩展程序的不同部分。
在本章的内容中,我们将使用 Vue Devtools 的 Chrome 扩展程序,但界面和功能应与其他可用资源相匹配,例如 Firefox 和 Electron 应用程序。
要开始使用,我们需要在我们的浏览器上安装应用程序;这可以通过在浏览器扩展商店中搜索 Vue.js devtools 来完成。如果您使用 Chrome,可以在 chromewebstore.google.com/category/extensions 找到它。
图 13.1:Chrome 扩展商店
有几个扩展名为 Vue.js devtools,但你想要安装由核心团队支持并开发的官方版本。这可以通过 vuejs.org 的勾选标记来识别。
在快速安装和浏览器重启后,你现在应该可以完全访问扩展。实际上,扩展不需要任何配置,直接使用即可。
要测试扩展是否工作,我们可以访问我们的伴侣应用网站 localhost:5173/ 并检查 Vue.js 扩展图标。如果扩展当前未工作,图标会变灰,如果 Vue Devtools 可用并正在运行,图标则会着色。
图 13.2:Vue Devtools 扩展
点击图标将确认扩展能够找到 Vue.js 并启用 Vue Devtools,如图 图 13.2* 所示。扩展作为 Chrome DevTools 中的一个新标签存在。这个标签应该会自动作为 DevTools 导航栏中最后一个可用的标签,名为 Vue。
图 13.3:Chrome DevTools 导航
什么是 Chrome DevTools?
如果你不知道 Chrome DevTools 是什么,或者以前从未使用过它,我建议你查看官方文档 developer.chrome.com/docs/devtools,并开始了解这个工具可以以不同方式使用的所有不同方法。
Vue Devtools 会自动监听 Vue.js 应用,并在开发模式下运行的所有 Vue.js 网站上立即可用。这是一个非常重要的点,因为扩展在用于生产的网站上不会工作。如果你尝试访问一个生产 Vue.js 网站(如 Vue.js 官方网站),扩展会加载但处于非活动状态。
图 13.4:访问生产构建网站时 Vue Devtools 的弹出窗口
现在是时候回到伴侣应用,打开 Chrome DevTools,并点击 Vue 选项卡,开始学习这个扩展能提供什么功能。
在本节中,我们将介绍扩展的主要部分,但正如我们将在本章后面提到的,扩展会自动扩展以提供有关不同包(如 Pinia 或 vue-router)的更多信息。
图 13.5:Vue Devtools
Vue Devtools 可以分为四个主要部分。我们将简要介绍所有这些部分,从左到右依次介绍:
-
主要导航:它位于左侧,作为一个垂直菜单。目前包含两个实体——组件和时序图。
-
应用列表:Vue.js 3 允许你在同一个网站上拥有多个应用,Vue Devtools 允许我们轻松地调试每一个。
-
主要内容:这是包括所选工具的区域。在图 13.5 中,我们选择了组件,因此本节显示组件树。
-
<SocialPost>组件。
现在是时候开始学习扩展的各个部分了。我们首先从默认部分开始,即组件和时间轴,然后进入如 Pinia 和 vue-router 等附加插件。
在 Vue Devtools 中调试组件
在本节中,我们将了解 Vue Devtools 为我们提供的调试和开发组件的功能。
在开发组件时拥有 Vue Devtools 这样的工具可以真正帮助您提高技能。Vue Devtools 可以帮助显示组件属性、事件和其他信息,这些信息有时在没有视觉线索的情况下可能难以发现或理解。
扩展组件部分的主体目标,可以通过点击主导航中的顶部图标访问,是提供我们对组件树的全面可见性和每个组件的详细信息。
让我们先看看组件树以及它为我们提供了哪些信息:
图 13.6:组件树
扩展提供的组件树与浏览器开发者工具提供的 DOM 树非常相似,主要区别在于 DOM 树显示 DOM 节点,而 Vue Devtools 由 Vue 组件组成。
在图 13.6 中,我们可以看到父组件和子组件之间的关系。实际上,我们可以看到<SocialPosts>有五个<SocialPost>子组件,而<SocialPost>有三个子组件。
树状图还显示了重要信息,例如由home :/紫色药丸定义的<RouterView>使用的路由,每个<SocialPost>个体的唯一键,以及每个<RouterLink>中定义的 URL。
能够可视化页面上渲染的组件很重要,但能够突出显示它们则更好。您可能一开始不会觉得这很重要,但能够在树中选择组件并在屏幕上看到它是一个非常实用的功能。
有两种方式可以突出显示组件。您可以直接在树中点击组件,或者通过启用在页面上选择组件来在页面上选择组件,如图图 13.7 所示。此功能可以通过点击图标或使用悬停在图标上时显示的弹出窗口中提示的键盘快捷键来启用。
图 13.7:页面中选择组件的按钮
当选择一个组件时,它将在屏幕上以绿色背景突出显示。
图 13.8:由 Vue Devtools 突出显示的社会帖子组件
现在我们已经可以读取树并选择单个组件,是时候深入扩展并查看每个组件显示的详细信息。
实际上,选择一个组件不仅通过将其高亮显示为绿色(如图图 13.8所示)提供给您一个关于组件的视觉线索,而且还暴露了关于它的内部信息列表。
侧边栏包括组件作用域内可用的所有信息,相当于能够打印出脚本设置中可用的所有内容。它包括基本信息,如props、Refs 和响应式,以及更高级的功能,如 Pinia 存储依赖项和路由信息。
图 13.9:Chrome DevTools 显示的社交帖子详细信息
能够访问屏幕上每个组件的内部信息是 Vue Devtools 最重要的强大功能。能够快速查看父组件发送给子组件的属性或特定 Ref的当前值,可以为您节省无数小时的调试时间。此外,查看组件状态也将帮助您更好地理解 Vue.js 框架。当我最初开始学习 Vue 时,我使用 Vue Devtools 来了解组件的工作原理,并发现 props、内部数据和生命周期之间的联系,并且从那时起我就向我的所有徒弟推荐它。
Vue Devtools 信息面板的用例
在本节中,我们将探讨几个使用 Vue Devtools 信息面板的用例。如前所述,信息面板包含许多有用的资源,但除非我们知道如何以及何时使用它们,否则它们毫无用处。
分析动态加载数据
在这个第一个场景中,我们将考虑一个开发者需要调试和理解 API 返回数据的用例。在我们的 Companion App 中,这种情况可能在开发社交帖子时出现。实际上,能够看到数据结构和值将有助于该组件的开发。
我们可以在图 13.10中看到 API 检索到的完整信息,即<SocialPosts>中的posts数组。
图 13.10:Vue Devtools 信息面板中显示的帖子信息
修改 Refs 和响应式数据
由于为我们的网页添加交互性的巨大可能性,JavaScript 已经变得流行。不幸的是,交互性并不总是容易实现,通常需要多次尝试才能使逻辑按预期工作。
在开发 Vue.js 组件时,你可能会发现自己处于需要测试不同场景的情况,这些场景要求数据处于特定状态。这有时可能非常耗时或复杂。
一个非常常见的场景是需要开发一个“错误”屏幕或电子商务购买后显示的“感谢”页面。为了开发这个组件,开发者需要重现多个步骤以达到所需的组件状态,这使得开发组件的过程非常缓慢。幸运的是,有了 Vue Devtools 的帮助,我们可以轻松地重现开发此组件所需的状态。
实际上,Vue Devtools 允许您实时修改 Refs 和 Reactive 数据。这可以直接在信息面板中完成,如图 图 13.11 所示。
您也可以修改 props
默认设置下,唯一可以修改的数据是 Refs 和 Reactive,但有一个设置允许您修改 Props。我个人不建议启用此功能,因为它可能会产生意外的后果,但了解其存在可能作为最后的手段是有帮助的。
图 13.11:Vue Vue Devtools 快速编辑
监控变化
修改组件内的数据不是开发复杂组件的唯一要求。实际上,另一个重要的功能是能够实时查看组件数据的值。例如,您可能想确保您的切换按钮能够成功工作,同时一个 Ref 从 false 变为 true,一个计算属性按预期更改其值,或者最后,通过重置所有数据到其初始状态来确保“重置”逻辑正常工作。可能性是无限的。
当使用 Vue Devtools 时,信息面板中显示的数据会在修改时自动实时更改。这使我们能够与应用程序交互并看到我们的交互产生的变化。
在本节中,我们通过学习如何安装和启用扩展来介绍了 Vue Devtools。然后我们定义了 Vue Devtools 的所有不同区域,并学习了如何使用它来突出显示和读取 Vue 组件树。最后,我们学习了如何通过访问信息面板来深入一个组件。在这个面板中,我们学习了如何查看数据、监控变化,甚至实时进行更改。
在下一节中,我们将介绍 DevTool 的一个更高级的部分——时间轴部分。
深入 Vue Devtools 时间轴标签
在上一节中,我们介绍了如何使用 Vue Devtools 的组件部分来分析和开发我们的 Vue 组件。在本节中,我们将继续探索主要导航,介绍下一个可用的部分——时间轴。
时间轴部分是事件和性能监控等工具的家园,这些工具有助于理解应用程序,让我们一窥框架引擎。
本节可能更适合高级用例,但了解工具提供的内容总是有益的,即使它们在日常活动中没有被使用。
可以从扩展的左侧主导航中访问时间轴标签,就在我们之前使用的组件标签下方。
时间轴面板包括三个部分。第一部分是图层部分,它显示我们将要收集和显示信息的所有不同图层。第二部分是时间轴本身。它以两种不同的方式提供,实际的时间轴或表格格式。最后,我们有信息面板,它与组件部分提供的信息面板非常相似。
图 13.12:Vue Devtools 时间轴面板
图层部分包括时间轴可以提供的所有不同信息。Vue Devtools 提供的图层在不同应用程序之间可能有所不同,因为它还可以包括一个已安装的包,如图 13.12中所示,其中 Pinia 和 Router 的数据也包含在图层菜单中。
图层可以产生大量数据(我们将在后面的部分中看到),通常隐藏大多数图层,只保留与你的特定任务相关的图层。
启用时间轴是资源密集型的,并且它不会一直自动运行。要启用时间轴,我们需要点击图层标题旁边的记录按钮。
能够开始和停止录制不仅可以帮助我们节省一些电池寿命;它还将确保时间轴上显示的数据尽可能紧凑。例如,如果你想分析用户创建帖子时发生的情况,你将启用它,创建帖子,然后停止录制。这样做将确保仅显示你感兴趣的事件。
调试帖子删除
为了更好地理解时间轴,我们将记录和分析在删除帖子时时间轴提供的信息,并试图确定如何将其用于未来的调试目的。
为了能够分析 Vue Devtools 时间轴部分的输出,我将在我们的应用中执行以下步骤。这些步骤将生成一个报告,我们可以用它来了解时间轴提供了哪些信息:
-
访问时间轴标签:打开 DevTools 并点击主导航中的时间轴标签。
-
选择图层:我们只会使用组件事件、性能和Pinia图层。从图层菜单中选择它们。
-
开始录制:现在,我们准备好记录我们的操作。通过点击记录按钮开始录制。
-
屏幕上显示的某个帖子中的“删除”图标。
-
停止录制:再次点击记录按钮以停止录制。
在完成前面的步骤后,时间轴应该显示类似于图 13.13所示的内容。
图 13.13:删除帖子后显示的时间轴信息
让我们看看记录活动后显示的信息。在检查记录会话的输出之前,让我们回到应用程序代码中,发现我们期望上述操作执行的动作。代码显示,当在<SocialPost>组件内部点击删除图标时,它应该发出一个名为"delete"的事件,然后用于<SocialPosts>触发 Pinia 操作,从而从存储中删除帖子。
如果我们回到我们的记录活动,我们可以看到时间线显示了所有三个层的一些活动。第一层是"delete",正如我们所期望的那样,是由<SocialPost>触发的。信息面板显示了有关事件的详细信息,例如参数,但在这个案例中,我们没有参数,数组为空。
接下来,我们将查看Pinia层显示的内容:
图 13.14:Pinia 的时间线事件
在选择removePost操作后,信息面板中显示了详细的信息。接下来,我们有一个突变;这是从removePost调用的,是实际从posts数组中删除帖子的操作。然后,我们有removePost end事件的removePost结束。总的来说,该操作耗时 0.8 毫秒完成。到目前为止,我们已经能够分析事件是否被触发,并跟踪 Pinia 操作的步骤。
跟踪您的操作
我们的removePost操作非常快,在不到 1 毫秒内完成。这并不总是如此,因为某些操作可能包括复杂的代码或外部操作,这可能会延迟其执行。使用时间线可以帮助您调试和修复慢速操作。
最后,我们将检查性能层显示的信息。就像其他两个层一样,当选择此层时,我们会看到单个事件及其信息。
图 13.15:性能时间线层
性能层用于高级用例,通常用于挖掘性能相关的问题或突出显示配置错误的代码库,这迫使您重新渲染一个或多个组件。
此层显示特定组件何时渲染或更新。在这种情况下,它只记录了四个事件,但对于大型应用程序来说,这可能会非常复杂。
能够分析渲染时间和组件渲染的次数对于性能问题严重的应用程序非常有用。计算属性或监视器的不正确使用可能导致组件的不必要重新渲染,性能时间线是帮助您调试和修复性能瓶颈的最佳工具。
在本节中,我们学习了如何使用 Vue Devtools 的时间线部分。我们定义了其结构,记录了一个示例测试以查看我们的应用程序性能,并了解了各个层显示的信息。
在下一节和最后一节中,我们将看到如何通过自定义插件扩展 Vue Devtools。
使用 Vue Devtools 插件分析附加数据
你可能已经注意到,从上一节中,Vue Devtools 不仅包括有关核心框架的信息,还公开了额外的信息,例如 Pinia 存储 和 vue-router。
这一切都是自动发生的,因为我们没有做任何额外的事情来扩展 Vue Devtools 的功能。事实上,插件实际上是我们在应用程序中安装的包的一部分,因此安装一个包有时会导致在 Vue Devtools 中显示额外的功能。
任何插件都提供不同的功能。在我们的例子中,Pinia 和 vue-router 都在时间轴视图中添加了一层,在组件详细信息面板中添加了信息,最后在主导航中添加了一个额外的选项卡。
在本节中,我们将深入了解两个可用的插件——Pinia 和 vue-router。
Pinia Vue Devtools 插件
我们将要分析的第一款扩展插件是由 Pinia 存储提供的。
以其标志性的菠萝标志而闻名的 Pinia 存储,可以从扩展的左侧主导航中访问。
这个插件可能会让你感到有些熟悉,因为它与组件部分的布局相似。
图 13.16:Pinia Vue Devtools 插件
在 Pinia 扩展中,我们能够检查我们的存储。该插件提供了通过选择 Pinia (root) 或我们状态管理中可用的单个存储来查看所有存储的能力。
信息面板中显示的数据按 state 和 getters 分隔,就像组件部分一样,它可以随时修改。
就像我们迄今为止探索的大多数功能一样,能够快速查看存储的当前状态并修改它们的值非常有价值。能够随时访问这些信息将节省你无数小时的开发时间。
vue-router Vue Devtools 插件
如你所记,在第十章中,我们介绍了在伴侣应用中引入的路由,我们必须创建一组规则,这些规则将由路由器使用以决定向用户显示哪个路由。
vue-router 插件可以帮助你在浏览器中直接可视化并检查这些规则。该插件提供了一个包含完整信息的路由列表,包括名称、正则表达式匹配和所需键。
图 13.17:vue-router 规则列表
图 13.17 中显示的列表显示了以下信息:
-
为特定路由定义的 URL (
/about)。 -
蓝色药丸中显示的路由名称(
user-profile)。 -
当前活动路由,由带有
active标签的蓝色药丸表示。 -
如
redirect和exact之类的额外信息。这分别表示存在重定向规则(/user/:userId)以及当前活动路由与规则匹配正则表达式完美匹配的概念。
现在我们已经定义了 vue-router 规则列表中可用的所有信息,是时候看看为每个单独的路由规则提供了哪些信息。让我们点击/user/:userId来查看它显示的内容:
图 13.18:vue-router Vue Devtools 插件的路由信息
该插件显示路由数组中可用的所有数据,例如path、name和redirect。此外,该插件还公开了更高级的数据,例如包用于决定显示正确路由所使用的正则表达式,以及关于键的详细信息,例如optional和repeatable,最后但同样重要的是,路径中每个条目的score值。
在您的职业生涯中,您可能不会在这个区域花费很多时间,但当你这样做时,它将拥有解决您问题的所有所需。
摘要
Vue 生态系统自豪地提供整个行业中最好的开发体验之一,Vue Devtools 可能是造成这种情况的罪魁祸首。
详细信息、性能指标和自动插件扩展使该扩展成为所有 Vue 开发者的必备品。本章最重要的收获是您需要在本地安装扩展并开始在项目中使用它。
在本章中,我们学习了如何安装和导航 Vue Devtools。我们涵盖了提供的各个默认部分——组件和时间线。最后,我们通过展示 Pinia 和 vue-router 在调试扩展中提供的附加信息,介绍了 Vue Devtools 中包插件的强大功能。
在下一章和最后一章中,我们将探讨未来的学习和资源,您可以使用这些资源继续您的 Vue 开发者之旅。
第十四章:高级阅读资源
如果你已经到达了这一章节,有很大可能性我很快就会在技术社区中以一个成功的 Vue.js 开发者的身份遇见你。这本书旨在为你提供足够的框架知识,让你在职业生涯中领先一步,而你坚持到书末的事实也凸显了你的决心,这在技术世界中是一个关键技能。
在本章中,我们将分享有用的资源,介绍高级 Vue.js 主题,并讨论提高你作为 Vue.js 开发者技能的方法。
本章分为以下部分:
-
可用的工具和资源
-
Vue.js 还剩下什么
-
为社区做出贡献
-
让我们回顾一下我们已经取得的成就
到本章结束时,你应该已经很好地理解了你所取得的成就,并且你将了解到你可以利用的、超越本书继续学习的资源。在本章中,我们还将介绍一些在这个阶段过于高级而无法涵盖的主题,但它们作为学习材料对继续你的训练是有用的。
可用的工具和资源
Vue.js 及其生态系统,就像其他前端技术一样,受到持续的变化和更新的影响,这有助于它保持相关性并改进其功能。在本节中,我们将学习如何了解 Vue.js 社区中的最新新闻。
我们将介绍新闻通讯、网站、社区成员等。在 JavaScript 这样一个快速发展的行业中保持最新信息对你的职业生涯至关重要。事实上,在你的日常工作中,你通常会处理当前或之前的软件或库版本,这不会让你接触到未来版本中包含的最新功能或改进。
无法在生产中使用即将发布的版本是正常的,对你来说,找到一种不同的方式来了解所有新的趋势和功能至关重要。为此,我们将讨论不同的资源,这些资源将帮助你保持最新。
Vue.js 文档
我们在开始本节之前不能不提到官方文档。Vue.js 的文档(vuejs.org/)总是最新且定义清晰,它应该是你查看有关新功能或更改信息的首选地点。
网站提供了许多有趣的内容,例如示例页面和教程,但以下链接应该被收藏并在日常工作中使用:
-
Vue 博客 (
blog.vuejs.org/):获取有关新版本和即将到来的更改的最新消息。这是你将首先听到有关新版本内容和信息的地方。 -
Vue.js API 速查表 (
vuejs.org/api/):一张包含所有 Vue.js API 快捷方式的单页文档。这对于您职业生涯的开始将非常有价值。作为开发者,您不需要记住所有内容,但要知道存在什么以及如何轻松访问,而这页正是这样做的。只需一键,它就能为您提供所需的信息! -
Vue Playground (
play.vuejs.org/):这可能不会为您提供新信息,但它对于尝试新事物而无需设置完整环境来说极其有用。链接也是可分享的,无需注册或验证。
Vue Discord (discord.com/invite/vue):还有什么比成为 Vue.js 社区的一员更好的学习方法吗?您可以加入官方的 Discord 聊天室,加入超过 10,000 人,包括核心成员和库维护者。Discord 频道包括许多涵盖主要框架及其库的聊天。这些聊天非常活跃,并由实际的 Vue.js 核心团队进行管理。
时事通讯
Vue.js 的官方文档是一个寻找所需或需要信息的绝佳地方,但您需要访问该网站来获取信息。在当今世界,我们期望信息能直接发送到我们的邮箱,那么通过订阅一些时事通讯来接收信息岂不是更好?
这里有一份我建议您订阅的 Vue.js 特定时事通讯列表:
-
每周 Vue 新闻:
weekly-vue.news/ -
Vue.js 开发者:
vuejsdevelopers.com/newsletter/
这两个时事通讯都提供了关于 Vue.js 生态系统的优质内容和最新信息,而且它们是免费的。这些时事通讯每月发送一次,非常适合周末阅读。
如果您有时间阅读每个分享的独立帖子,您当然应该这样做,但如果您没有时间,我建议您至少花时间浏览一下帖子的名称,因为它们可以为您提供重要信息,例如主要发布或重大更改。
社区成员
要成为一个开发者而不加入社交媒体平台是非常困难的。我个人在 X (x.com)上以用户名@zelig880非常活跃。在本节中,我将突出一些重要的社区成员,您应该在您选择的社交媒体上亲自关注他们。
这些人就是 Vue.js 独特之处。他们通过在会议上分享知识、成为核心存储库的活跃成员以及作为活跃的库维护者来帮助社区成长。
我打算在每个 X 用户名旁边放置名字,以便您在网上搜索时更加方便,因为有些人可能有非常常见的名字:
-
@youyuxi): 艾文是 Vue.js 社区的杰出成员,不仅因为他创建了该框架,还因为他积极参与社区活动。艾文在会议和社交媒体上都非常活跃,并且是第一个分享即将到来的新闻的人。 -
@antfu7): 安东尼是 Vue.js 中许多库的幕后推手,包括 VueUse 和 Vitest。安东尼不仅创建了令人惊叹的库,而且还喜欢尝试新事物,因此他的时间线总是充满了美好的想法和创作。 -
@posva): 埃德华是多利亚的创建者,也是 Vue 生态系统的一位活跃成员。埃德华在世界各地旅行,参加各种全球范围内的 Vue.js 会议。 -
@danielcroe): 丹尼尔是 Nuxt.js 核心团队成员,并且是一位非常活跃的会议演讲者。丹尼尔还喜欢直播他的工作。观看像丹尼尔这样的经验丰富的人尝试完成任务,更重要的是,在线调试问题,是学习的一种极好方式。 -
@_jessicasachs): 杰西卡曾在多家公司工作,包括 Cypress,并且是一位活跃的会议演讲者。
我可以继续添加更多社区中的杰出成员,但我认为这个列表对你开始了解是非常好的。跟随这些人将为你提供框架生态系统中的良好见解和更新。
请求评论 – RFC
为了完成这一部分,我们将分享一些更高级的内容,但它们仍然可以帮助你感受到自己是社区的一部分:请求评论(RFCs)。Vue.js 是唯一一个没有由科技巨头支持的顶级框架。独立使得它可以根据社区的需求做出选择,而不是取悦股东(我知道大多数框架都不是为了盈利,但他们的决策仍然在幕后进行)。
如果你问艾文·尤如果他是 Vue.js 的所有者,他会说不是,并且他会澄清 Vue 是属于其社区的。在生态系统中度过了多年,我可以证实这不仅仅是他所说的话,而是他和核心团队成员构建框架的方式。
对于 Vue.js 的所有主要版本,Vue.js 都积极使用 RFC 来分享其核心引擎中的想法和变化。这个仓库见证了众多对话,其中一些非常热烈,但最重要的是展示了核心团队对帮助的开放态度。
包含所有 RFC 的仓库可以在 github.com/vuejs/rfcs 找到。
我不期望你每天都查看仓库,但当你通过新闻通讯或社区成员分享新主题时,查看评论和讨论是非常有益的。
对于大多数开发者来说,这些 RFC 中共享的对话和代码可能相当高级,但阅读评论可以帮助你了解人们如何使用框架,并在这一过程中成长。
这是本节最后一个主题,我们分享了不同的方式,你可以通过这些方式在 Vue.js 生态系统中保持活跃,我们讨论了官方文档的有用性,讨论了如何通过订阅最新的通讯来获取信息。然后我们有一个关于社区成员的部分,介绍了一些重要的人物,你应该关注他们,以便能够第一时间了解框架的新闻和更新。最后,我们讨论了 RFC,并学习了它们如何让你窥见框架的未来。
在下一节中,我们将学习在 Vue.js 中我们还需要学习什么,以及作为 Vue.js 开发者,你可能会遇到的其他主题。
Vue.js 还剩下什么?
在这本书中,我们已经涵盖了众多不同的主题,并且对 Vue.js 有了足够的了解,能够处理你未来的任务或项目。但我们只是学习了框架的核心特性,而在你的开发旅程中未来可能遇到的某些高级主题仍然有待学习。
这些主题的重要性并不亚于你已经学过的那些,但它们是次要的,因为它们是在掌握了基础知识之后才出现的。在本节中,我们不会深入探讨每个主题的细节,而只是提供一个简要的介绍。
本节背后的想法是为你提供对许多你可能在未来作为 Vue.js 开发者职业生涯中需要学习的主题的基本理解。其中一些主题相当高级,而其他一些可能只在特殊用例中需要。
我们将把这些主题分为几个部分:杂项、核心、Pinia 和 Vue-router 特性。让我们首先看看为了使我们的伴侣应用(Companion App)达到生产就绪状态,我们需要学习哪些主题。
杂项
以下主题对于应用达到生产就绪状态是必要的,但对于刚开始接触框架的人来说可能过于高级。
列表中包括一些大主题,例如认证,这些主题可能需要整本书来专门介绍。我们之所以要介绍这些主题,并不是要你现在就去学习它们,而是让你意识到它们的存在,并在你需要实现这些功能的时候去学习它们。
让我们看看我们在应用中遗漏了什么:
-
认证:像伴侣应用这样的应用在未设置认证之前是无法完成的。幸运的是,有许多服务,如Auth0、Supabase和AWS Cognito。这些工具拥有优秀的文档和入门指南。使用认证提供者是非常好的,不仅因为它能节省你的开发时间,而且因为将用户名和密码托管在服务器上是一个安全风险,也是一个复杂且你应该在职业生涯初期避免的任务。
-
环境变量:在我们的应用程序中,我们直接在应用程序中设置了诸如令牌和 URL 之类的变量,但这是不正确的。我们应该做的是使用环境变量来安全地存储这些值,远离仓库。Vite 提供了一种简单的方式来定义环境变量(
vitejs.dev/guide/env-and-mode)。 -
日志记录/错误报告:一个应用程序在投入生产之前绝对不能没有良好的日志记录。创建一个无错误的程序是一个神话,而且你的用户遇到问题的可能性总是存在的。如今的大多数托管环境都提供了开箱即用的日志记录功能,但我更喜欢使用像Sentry这样的工具,这些工具可以帮助你快速找到应用程序中的问题,节省你的时间和金钱。
-
CI/CD:这个术语代表持续集成/持续交付。它的目的是简化并加速软件开发的生命周期。在将应用程序推广到生产状态之前,你可能想要设置一个自动的管道,快速将你的代码交付到生产环境中。幸运的是,还有一些像Netlify、Vercel和AWS amplify这样的服务提供现成的解决方案。
-
组件库:乍一看,创建组件可能看起来像是一项简单的任务,但事实并非如此。一个完整的组件库可能需要数千个小时,这对大多数开发者来说是不切实际的。使用现有的组件库,如Vuetify(
vuetifyjs.com/)或Quasar(quasar.dev/),可以加快我们的开发速度,并帮助我们专注于最重要的事情:应用程序的逻辑。如果你正在寻找特定的组件,你可以在 vuecomponents.com 上搜索,这是一个 Vue.js 生态系统内可用的组件的活跃列表。
核心功能
现在是时候介绍 Vue.js 的核心功能了,这些功能可能在未来的任务中很有用。就像 Vue.js 生态系统中的其他一切一样,这些核心功能都有很好的文档,并且可以从官方 Vue.js 文档中学习:
-
SFC(单文件组件)的
<script>标签,但背后的推理以及你使用它的方式可能需要进一步的学习。了解可组合式组件的最佳方式是找到一些可以添加并用于你应用程序的可组合式组件,而VueUse就是这样一个地方。这是一个由 Anthony Fu 和许多其他活跃贡献者创建和维护的数百个可组合式组件的列表。将这个列表添加到你的项目中不仅会加快你的开发速度,而且还会帮助你完全理解可组合式组件是如何工作的以及它们解决了哪些问题。作为额外的奖励,VueUse 是开源的,他们的代码可以从网站上轻松获取。 -
<component :is="componentName" />可以根据用户偏好有条件地渲染表格或列表,或者将<input>转换为<textarea>。就像本节中的其他主题一样,这个内置组件解决了一个非常具体的问题。 -
作用域插槽:作用域插槽是一个非常高级的主题。除非在特定情况下,否则你很少需要它。作用域插槽解决了一个非常具体的问题,但不幸的是,它不是最容易使用或学习的。记住它们的存在,如果你在尝试使用插槽却无法实现时,搜索它们,因为它们很可能是你问题的解决方案!但正如我所说的,现在避免使用它们,如果你需要,请寻求资深开发者的帮助。
-
<Transition>帮助我们结合 CSS 动画的力量和 Vue.js 模板功能的灵活性,如v-if。 -
Teleport:请不要担心,我们不会开始谈论科幻。Teleport 是 Vue.js 的另一个内置组件,它允许你将 Vue.js 组件“传送”到 Vue.js 应用程序外部的 DOM 的不同节点。这个组件解决了组件的逻辑位置可能与其视觉位置不匹配的具体问题。一个常见的用例是对话框,其中模态按钮和组件将位于同一组件内(逻辑位置),而对话框本身的视觉应该不在 DOM 中嵌套,而应该在树结构中更高一层。
我们刚刚分享的先进组件和技术列表只是冰山一角。Vue.js 还有更多要提供,以及许多你将在职业生涯中发现的先进主题。
Pinia 的功能
在第十一章中,我们介绍了使用官方库 Pinia 进行状态管理。在本节中,我们将分享一些我们在这本书中之前未曾见过的几个高级主题。
正如我们之前提到的,这些是值得了解的好主题,但它们不是你在旅程开始时必须学习的。让我们看看 Pinia 还能提供什么:
-
$Patch:Vue.js 最大的特性是其响应性系统。正如我们在上一章所看到的,Vue 的响应性也通过 Pinia 得到了扩展,它提供了一个对变化反应迅速的存储库。为了确保你的应用程序性能良好,并且响应性不会变成一个负面影响,Pinia 提供了一个名为store.$patch的方法。这个方法可以用来同时修改多个存储条目。除了接受一个部分状态对象外,这个方法还接受一个可以用来更新存储库复杂部分(如数组或嵌套对象)的方法,并确保所有这些都在单一的状态变化中完成。 -
$reset(): 在 Pinia 存储中可用的另一种方法称为$reset()。此方法允许你将存储的状态恢复到其初始状态。这对于需要清理存储以完成动作的动态表单和其他交互性非常有用。调用此方法仅将状态对象返回到其初始值。 -
$subscribe: 在本节中我们讨论的 Pinia 方法中,这可能是最复杂的一个。Pinia 提供了两种不同的方法来订阅存储的一部分。第一种是$subscribe方法。这允许你监视 Pinia 存储的变化。第二种称为$onAction,用于监听 Pinia 的动作。使用这两种方法订阅 Pinia 存储不是日常任务,通常用于解决特定问题。
在 Pinia 之后,是时候查看 vue-router 并完成本节的最后部分了。
Vue-router 特性
就像 Pinia 一样,我们早在 第十章 中介绍了 vue-router,并学习了如何在我们的伴侣应用中实现基本路由功能。在本节中,就像我们在前面的章节中所做的那样,我们将介绍路由库提供的先进技术:
-
路由懒加载: 我们将要分享的第一个功能是路由懒加载。Vue-router 允许你指定一个异步加载的路由。异步加载路由可以通过减少访客最初需要下载的 JavaScript 包的大小来提高我们应用程序的性能,并在需要时加载路由。
-
守卫: 路由守卫允许 vue-router 通过重定向或取消导航来“守卫”用户导航。路由守卫对于执行基于角色的导航或身份验证非常有用。使用路由守卫,你可以在每次导航前后运行逻辑,从而完全控制用户可以看到的内容。
-
活动链接: 在主导航或站点内显示活动状态是一种常见做法。Vue-router 通过提供活动链接类来简化这一过程。这些类会自动分配给与当前 URL 完全或部分匹配的路由链接。
-
使用单个出口的
<RouterView>,但通过命名视图,我们可以提供布局的不同命名部分。我们可以使用这个功能来定义一个带有侧边栏和主体内容的布局,并根据我们所在的路线切换侧边栏。
关于我们刚刚讨论的功能的更多信息,可以在官方 vue-router 文档中找到:router.vuejs.org/guide/。
向社区回馈
在整本书中,你听到了 Vue.js 社区是多么的出色,亲眼目睹了核心团队和包维护者所做的大量工作。Vue.js 框架有很好的文档和良好的维护,开发体验得到了很好的定义,同样适用于核心维护的包,如 vue-router 和 Pinia,以及生态系统中的外部包,如 VeeValidate 等。
这种一致性不是免费的,背后有很多工作要做,以确保社区感到被接受和支持。有时这项工作是由开发者完成的,由于开源的性质,他们不会因为他们的贡献而获得报酬。
在阅读这本书之后,你已经成为了 Vue.js 社区的一个有机组成部分,你应该尽你所能为社区做出贡献并帮助维护它。这样做不仅会支持生态系统和未来的开发者,而且也会帮助你成长。
让我们看看社区成员可以以哪些不同的方式参与社区:
-
文档:如果你不熟悉开源,你可能已经多次听到人们请求帮助文档。这主要是因为这确实是开发者可以采取的最重要且最容易的行动,以回馈开源社区。作为 Vue.js 社区的新成员和该技术的首次用户,你将能够评估哪些文档写得很好,哪些可能需要改进。所有 Vue.js 文档都是开源的,所以如果你发现需要修复的地方,请创建一个 PR。
-
重现错误报告:当我最初开始学习 Vue.js 时,这曾是我学习资料的主要来源之一。事实上,在核心框架及其库中提出了许多错误,其中一些没有伴随可重现的代码,或者有错误的重现。尝试重现已分享的错误是深入了解 Vue.js 的非常好的方法。这些问题可以在带有标签 need repro 的主要仓库中找到(
github.com/vuejs/vue/labels/need%20repro)。 -
举办聚会:没有比举办本地聚会更好的方式来帮助 Vue.js 发展了。举办聚会可能不是每个人都适合的,但如果你有合适的个性和兴趣,这将是非常有回报的。你不仅会在行业内获得知名度,而且在与 Vue 社区中遇到其他人的同时,你也会在活动中了解到他们。
-
成为活跃的 Discord 成员:在本章的第一部分,我们提到 Discord 作为一种在社区中保持信息更新的方式。在本节中,我们再次提到 Discord,作为您为社区做出贡献的一种方式。您可能一开始无法直接提供帮助,但随着您获得更多知识,您应该尝试帮助整理聊天中分享的一些问题和讨论。
-
捐赠:如果您现在刚开始您的开发生涯,这可能不是您负担得起的事情,但随着您职业生涯的发展,您应该考虑这一点。Vue.js 没有大公司支持它,而且大部分核心维护者的工资都是由像我这样的人和您这样的小捐赠组成的。每一笔小捐赠都有助于。
无论您如何做,最重要的是您尝试为这个了不起的社区做出贡献,并成为其中活跃的一部分。在本书的下一部分和最后一章中,我们将总结本书过程中所取得的成果。
让我们回顾一下我们已经取得的成果
您已经到达了这本书的最后一章,您准备去独自体验 Vue.js 了。在学习新技术时,就像您在这本书中所做的那样,这并不容易,您可能需要回顾并重新学习我们在书中讨论的一些主题。
在本节中,我们将快速回顾我们已经学到的内容以及我们可以用它完成的事情。多次回顾一个主题不仅可以帮助您记住它,还可以帮助您理解其含义,从而允许您将其应用于不同的用例。
Vue.js 的一切
我们从学习 Vue.js 及其最重要的反应性系统开始这本书。在不同的框架中,Vue.js 的反应性非常强大,如果您想提高自己的技能,理解它是必须的。
接下来,我们学习了 Vue.js 的生命周期。该框架在一个定义良好的周期上工作,了解这一点对于您能够创建高性能的应用程序至关重要。
最后,我们通过学习 Vue.js 组件的结构,也称为单文件组件(SFC),完成了关于 Vue.js 的部分。我们了解了它的组成,并在书中广泛使用了它。
从基础到更深入
在我们伴侣应用的帮助下,我们学习了如何将静态 HTML 转换为动态的 Vue.js 组件。我们通过引入字符串插值、Refs 和 Reactive等概念,了解了框架的基础。
然后,我们通过引入一些 Vue.js 内置指令,继续增加我们应用程序的复杂性。有了v-if和v-for等特性,我们能够增加我们组件的功能性,同时仍然能够编写优雅的组件。
最后,我们介绍了computed和methods。这些功能使我们能够为我们的组件添加逻辑。
借助到目前为止所学的知识,我们可以将一个非常静态的网站转变为一个动态且功能强大的网络应用,这将超越简单的静态 HTML 网站。
从组件到组件
从第六章开始,我们开始从单个组件扩展到多个组件的学习。我们通过引入 props 和 events 来实现这一点。这使得我们能够创建能够相互交流和依赖的组件。
在这些额外主题的基础上,我们还有机会重新介绍并使用我们在书的开头学到的 Vue.js 生命周期。
为了结束这一部分,我们开始通过异步加载数据来连接我们到目前为止所涵盖的所有不同主题。为此,我们必须使用方法、生命周期、事件、Refs 和 props。
在本节结束时,即第七章,我们总结了 Vue.js 的基础。
Vue.js 生态系统
在掌握 Vue.js 的基础之后,是时候继续前进并介绍重要主题,如测试、路由和状态管理,并从核心框架中移开。
在基本主题之后的章节中,我们首先学习了如何在我们的应用程序中使用 Cypress 和 Vitest 编写单元测试和端到端测试。
接下来,我们介绍了由 Vue 核心团队成员维护的两个库:Pinia 和 vue-router。通过 Pinia,我们介绍了状态管理的概念,并重构了我们的 Companion App 以使用 Pinia 来支持其状态,而 vue-router 则用于实现路由系统,将我们的单应用转变为具有多个页面和嵌套路由的复杂应用。
在路由和状态之后,是时候通过学习如何管理表单来添加一些用户交互性了。我们通过引入 V-Model 的双向绑定概念来实现这一点,然后讨论了使用外部库如 VeeValidate 如何帮助我们开发复杂且结构良好的表单。
最后,我们离开了编码,学习了如何使用 Vue.js 开发工具扩展来帮助我们开发和调试 Vue.js 应用程序。
只不过是冰山一角
与书籍一起,我们讨论了可能的练习和学习,以帮助你更好地理解各种主题。
书中分享的信息和提供的练习只是冰山一角。要完全理解 Vue.js 的工作原理并熟练运用它,你将不得不继续练习并使用在这本书中学到的知识。
我建议你继续练习,也许通过设定具体的每日参与目标或加入如 #100daysofcode 这样的社区。你可以通过使用 Vue.js Playground 创建非常小的概念证明来测试你的知识,或者复制现有应用程序的某个小部分,包括所有知识和交互性,以加深你的理解。在你感觉你对 Vue.js 的基础及其生态系统有了足够的理解之后,尝试克隆一个现有的网络应用程序是个不错的想法。这是一个非常有用的练习,可以帮助你专注于 Vue.js 学习的实践部分,而无需从头开始想出一个应用程序的想法。一些可以复制的应用程序示例包括社交媒体平台、新闻平台和天气应用程序。它们提供了良好的交互性,并提供免费 API 和资源。
摘要
经过 14 章旨在提高你作为 Vue.js 开发者技能的章节后,我们终于到达了这本书的结尾。完成这本书可能与你开始 Vue.js 职业生涯的开始有关。
你在这本书中学到的知识应该足够你开始你的职业生涯,但仅仅依靠这些知识是不够的。你现在需要的是真正的投入和一致性,将在这里学到的所有知识应用到实际项目中。在这个过程中,你可能需要回顾和重新阅读某些章节或部分,或者进一步阅读以加深你的知识(始终从 Vue.js 官方文档开始你的研究)。
Vue.js 是一个出色的框架,我可以向你保证你做出了一个正确的选择。正如我们一次又一次提到的,使 Vue.js 独特的不只是它的代码和功能,还有围绕它构建的完整生态系统和社区。
找到一个与社区互动的好方法将帮助你保持动力和参与度,但最重要的是,与其他 Vue.js 开发者互动可以帮助你克服你在整个职业生涯中可能会遇到的问题和障碍。
所有 Vue.js 核心库及其生态系统都是建立在框架核心逻辑之上的。这不仅使得你在切换库时感到熟悉,而且意味着掌握框架的核心基础将帮助你理解整个生态系统。我有幸见过许多 Vue.js 开发者,而让最资深开发者脱颖而出的特质就是他们对 Vue.js 的深入理解。这并不需要你学习实际的代码库,只需真正集中精力并理解前两章涵盖的主题,例如响应性和生命周期。Vue.js 的响应性将帮助你理解何时更新以及为什么更新,而生命周期将提供对框架如何重新渲染以及它在此过程中遵循的流程的理解。我通常喜欢将开发比作一场舞蹈。有各种不同的舞步,你一开始可能并不知道所有舞步,但最重要的是你知道如何聆听音乐并随之舞动。随着你不断练习,其他的一切都会随之而来。学习 Vue.js 的基础就像能够聆听音乐一样,而且对它的深入了解将使你的旅程更加容易。
在这个阶段,我只能说祝你们在未来的道路上一切顺利,并希望 Vue.js 能够成为你们职业生涯的一个好选择。