Vue3-初学者指南-全-

Vue3 初学者指南(全)

原文:zh.annas-archive.org/md5/f9e4bd72d595e5007a42cab59e1b51c3

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

过去十年,JavaScript 框架的数量激增,旨在通过更好的用户体验(UX)和增强的客户交互性来改善客户端体验。其中最成熟和最受欢迎的框架之一是 Vue.js。

Vue.js 是一个用于构建用户界面的 JavaScript 框架。它建立在标准的 HTML、CSS 和 JavaScript 之上,提供了一个声明式、基于组件的编程模型,帮助你高效地开发任何复杂性的用户界面。

Vue.js 因其充满活力的生态系统、愉悦的开发体验和对简洁的关注,已成为所有级别开发者的最爱。

这本名为Vue.js 3 for Beginners的书是掌握这个框架的全面指南。本书的目标是带你从 Vue.js 的核心概念开始,逐步构建一个真实世界的应用。

现在有大量教程、文章和文档分享 Vue.js 的功能,但以下是这本书与众不同的地方:

  • 通过实践学习:整本书中,你将一边构建伴随应用,一边学习理论概念。这种动手实践的方法确保你掌握不同章节之间的联系以及 Vue.js 组件是如何协同工作的。

  • 适合初学者:无论你是 JavaScript 框架的新手还是有其他框架的经验,这本书都为 Vue.js 提供了一个坚实的基础。我们从基础知识开始,随着你的进步,逐渐引入更高级的技术。

  • 真实世界案例:我们将使用清晰实用的案例来说明关键概念,使 Vue.js 更易于理解和吸引人。在许多情况下,我们将涵盖多种方法,包括它们的各自优缺点。本书还包括许多提示,帮助你获得更深入的了解。

  • 经验之谈:这本书是许多成功项目和众多受训学徒的结果。书中介绍和解释主题的流程在我的职业生涯中经过了多次迭代。

到本书结束时,你不仅会对 Vue.js 有深入的理解,还会拥有一个功能齐全的应用程序,你可以使用或修改它用于自己的项目。

这本书面向谁

有志于成为网页开发者、学生、爱好者,以及任何希望从头开始学习 Vue.js 并渴望使用现代流行框架深入前端开发的人都将从这本书中受益。本书要求对前端技术(如 HTML、CSS 和 JavaScript)有基本的了解。主题通过将问题分解成小而易于理解的单元以模块化的方式介绍。

本书涵盖了 Vue.js 框架的许多主题,并定义了良好的标准,这些标准对希望确保正确使用框架的现有 Vue.js 开发者也有益。

这本书涵盖的内容

第一章**, 探索本书布局和伴侣应用,涵盖了伴侣应用的详细信息及其将包含的内容。本章定义了本书中使用的结构和方法论,并包括与框架无关的重要主题,如组件化架构、原子设计和核心 Web 开发领域。

第二章**, Vue.js 的基础,专注于提供有关 Vue.js 框架的重要信息。本章涵盖了诸如响应式系统、组件组合和框架生命周期等主题。

第三章, 使我们的 HTML 动态化,是我们开始构建伴侣应用所需工作的起点。我们将学习如何初始化 Vue.js 应用,并了解将静态 HTML 文件转换为动态 Vue.js 组件的基本步骤。

第四章**, 利用 Vue 的内置指令实现轻松开发,介绍了 Vue.js 最重要的功能之一:内置指令。在本章中,我们将学习如何通过引入最重要的指令来增强组件的交互性和动态性。

第五章, 在 Vue.js 中利用计算属性和方法,通过介绍不同的技术来处理组件内部的数据和逻辑,使我们的组件更加简洁易读。本章介绍了包括 Ref 和响应式数据计算属性和方法在内的主题,这些是 Vue.js 逻辑的支柱。

第六章**, Vue.js 中的事件和数据处理,通过教授我们如何处理组件之间的通信,扩展了我们对 Vue.js 框架的了解。我们还将深化对 props 的了解,并介绍自定义事件的概念。

第七章**, *使用 Vue.js 处理 API 数据和异步组件的管理**,教导我们如何处理外部异步数据。我们将实践生命周期概念,并学习如何使用watch<suspense>处理异步数据。

第八章, 使用 Vitest 和 Cypress 测试您的应用,将通过介绍测试来提供确保代码质量所需的工具。本章涵盖了测试的一般知识,然后定义了使用 Vitest 的单元测试和 Playwright 的端到端测试,以测试现有的伴侣应用组件。

第九章**, 高级 Vue.js 技术介绍 – 插槽、生命周期和模板引用,将您带回 Vue.js 的核心,并介绍高级技术。这些章节涵盖了插槽、生命周期和模板引用等高级主题。

第十章使用 Vue Router 处理路由,介绍了第一个外部包:vue-router。在这一章中,我们将向我们的伴侣应用程序添加路由。我们将学习如何定义我们的路由以及如何在应用程序内进行导航。

第十一章使用 Pinia 管理我们的应用程序状态,专注于状态管理并介绍了一个新的核心包:Pinia。我们将学习如何状态管理可以简化我们的应用程序,并在我们的伴侣应用程序中创建多个示例存储来了解 Pinia 提供的不同功能。

第十二章使用 VeeValidate 实现客户端验证,介绍了表单处理和验证的主题。我们将回顾 Vue.js 原生的处理表单的工具,如 v-model,并通过介绍 VeeValidate 来涵盖高级案例。

第十三章使用 Vue Devtools 揭示应用程序问题,从我们的伴侣应用程序的开发中退一步,专注于提高我们的技能和对 Vue.js 框架的理解所需的调试技术。

第十四章未来阅读的高级资源,通过一些进一步阅读和材料来结束我们的旅程,这些材料将帮助您继续学习 Vue.js 框架。

为了充分利用本书

本书详细介绍了与 Vue.js 相关的所有内容以及任何引入的附加包,但要求读者具备前端开发的基本理解。为了充分利用本书,需要具备 HTML、CSS 和 JavaScript 的现有知识。

本书涵盖的软件/硬件 操作系统要求
Vue.js 3 Windows, macOS, 或 Linux
Vite Windows, macOS, 或 Linux
Crypress Windows, macOS, 或 Linux
Vitest Windows, macOS, 或 Linux
Vue DevTools Windows, macOS, 或 Linux
Vue-router Windows, macOS, 或 Linux
Pinia Windows, macOS, 或 Linux
VeeValidate Windows, macOS, 或 Linux

本书包含了在旅程过程中安装和配置软件所需的所有信息。有一些依赖关系在书中没有介绍但却是必需的。第一个是 Node.js。应用程序可以与任何主要版本的 Node 一起工作,因此您可以安装最新稳定版。下一个要求是 IDE。我将使用 Visual Studio Code,但您可以使用任何其他 IDE。

如果您使用的是本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误

书的最后一章为你提供了如何继续你旅程的指导。我建议你继续你的学习之旅,以不断提高你在 Vue.js 方面的技能。

下载示例代码文件

你可以从 GitHub 下载本书的示例代码文件 github.com/PacktPublishing/Vue.js-3-for-Beginners。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富的书籍和视频目录的代码包,可在 github.com/PacktPublishing/ 获取。查看它们吧!

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“Vue.js 试图使用它拥有的数据渲染应用程序,导致缺失的数据被设置为null。”

代码块设置如下:

<template v-if="comments.length === 0"></template>
<template v-else></template>

当我们希望引起你对代码块中特定部分的注意时,相关的行或项目将以粗体显示:

const posts = reactive([]);
const fetchPosts = () => {
   ...
}
fetchPosts();

任何命令行输入或输出都按以下方式编写:

npm install 

粗体:表示新术语、重要单词或你在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“由于关于页面不需要覆盖页脚,所以我们将其排除在我们的实例之外,以便可以渲染默认值。”

小贴士或重要提示

看起来像这样。

联系我们

我们读者的反馈总是受欢迎的。

一般反馈:如果你对本书的任何方面有疑问,请通过电子邮件发送给我们 customercare@packtpub.com,并在邮件主题中提及书名。

勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在这本书中发现了错误,我们将不胜感激,如果你能向我们报告这一点。请访问 www.packtpub.com/support/errata 并填写表格。

盗版:如果你在网上遇到我们作品的任何形式的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件发送给我们 copyright@packt.com,并提供材料的链接。

如果你有兴趣成为作者:如果你在某个领域有专业知识,并且有兴趣撰写或为本书做出贡献,请访问 authors.packtpub.com

分享你的想法

读完 Vue.js 3 for Beginners 后,我们很乐意听到你的想法!请 点击此处直接进入此书的亚马逊评论页面 并分享你的反馈。

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载本书的免费 PDF 副本

感谢您购买本书!

您喜欢随时随地阅读,但又无法携带您的印刷书籍到处走?

您的电子书购买是否与您选择的设备不兼容?

请放心,现在,随着每本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠不会就此停止,您还可以获得独家折扣、时事通讯和每日免费内容的每日访问权限

按照以下简单步骤获取福利:

  1. 扫描二维码或访问以下链接

二维码

packt.link/free-ebook/9781805126775

  1. 提交您的购买证明

  2. 就这样!我们将直接将您的免费 PDF 和其他福利发送到您的邮箱

第一部分:Vue.js 入门

在本书的第一部分,我们将介绍完成学习之旅和更好地理解 Vue.js 框架所需的基本知识。本书将为构建 Companion App 所使用的架构奠定基础,并提供关于 Vue.js 及其核心逻辑的基本知识。

本部分包含以下章节:

  • 第一章**,探索本书布局和 Companion App

  • 第二章**,Vue.js 的基础

第一章:探索书籍布局和配套应用程序

Vue.js 是 JavaScript(JS)生态系统中的一个极其流行的框架。近年来,由于其简洁性、出色的文档以及最终其出色的社区,它获得了大量的关注。如果你现在开始网页开发,或者是从其他框架或语言过渡过来,Vue.js 是一个很好的选择。

在我们深入本书的主要内容之前,了解书籍的结构以及将用于解释这个出色框架不同主题的方法非常重要。

为了简化 Vue.js 的学习,并使书籍更加有趣和互动,本书围绕配套应用程序的创建和增强而构建。

Vue.js 3 入门》将主要关注框架及其核心库,并且不会涵盖 HTML、CSS、JS 和 Git 等基本开发知识。为了理解本书的内容,需要具备这些四个主题的基本知识。

本书的第一部分将涵盖我们学习旅程的一个重要方面,并为你提供利用本书内容所需的重要理论知识;然后,我们将通过介绍框架及其核心概念在第二章中跳入 Vue 的具体细节。最后,从第三章开始,我们将逐个组件地开始构建我们的应用程序。

在本章中,我们将涵盖以下主题:

  • 配套应用程序

  • 网络应用程序的核心区域

  • 组件化架构

到本章结束时,你将了解在本书的学习过程中我们将要构建的内容,并涵盖一些理论方面,这些方面是我们充分利用 Vue.js 框架所必需的,例如组件化架构和配套应用程序背后的架构决策。

技术要求

伴随本书的应用程序是使用免费软件和 API 构建的,不会要求你购买任何东西。然而,有一些特定的技术要求需要你遵循:

  • Visual Studio Code 或另一个等效的 IDE(集成开发环境)

  • Volar Visual Studio 代码扩展

  • 更新到最新版本的浏览器(我建议使用 Chrome 或 Firefox)

  • Node 16+

  • 在您的机器上安装了 GIT 或 Git GUI图形用户界面)例如 GitKraken

配套应用程序

学习一门新的语言或框架并非易事。互联网上有许多免费资源,例如文档、博客和 YouTube 视频,但我相信学习新技术需要实践,而最好的方式就是一起构建一个生产就绪、性能良好、可扩展的社会媒体应用程序。

应用程序将非常类似于社交媒体平台 X(以前称为 Twitter)。我们将从一个干净的画布开始,逐渐添加更多功能和功能,直到应用程序完全工作并准备好添加到你的作品集并在你下一次求职面试中展示。

每章都将有一组部分,帮助你导航本书。这不仅确保你始终能够跟随并清楚地理解章节的范围,而且在你阅读完本书后,还可以作为参考使用,如果你需要的话,可以跳转到特定的章节。

每章都包括以下部分:

  • 章节开始的分支

  • 伴侣应用程序的当前状态

  • 定义当前章节将添加和实现的内容

  • 多个解释和编码部分

  • 本章我们学习到的 Vue.js 主题总结以及术语表

伴侣应用程序的功能

如前所述,我们即将构建的应用程序将非常类似于现有的社交媒体应用程序。为了确保我们涵盖 Vue.js 的大多数特性和其生态系统,我们有时可能会过度设计特定的组件或功能,但在此情况下,将会指出,以便你全面了解是否是未来遵循的良好实践以及正确的实现方式。

通过跟随本书,你将学习以下内容:

  • 如何使用基于组件的架构来构建网页应用程序

  • 如何使用 Vue.js 创建简单和复杂的 HTML

  • 如何做出正确的决策,使你的应用程序具有性能和可扩展性

  • 如何在组件之间进行通信

  • 如何使用外部 API 加载动态数据

  • 如何使用 Pinia 进行状态管理

  • 如何使用 vue-router 实现多页面(路由)

  • 如何使用 Vitest 和 Cypress 测试你的应用程序

  • 如何使用 Vue.js 有效地创建表单

  • 如何使用 Vue 调试器调试你的应用程序

上述列表只是本书我们将实现内容的概述,我们将通过构建伴侣应用程序来使学习变得有趣和互动。

应用程序代码

应用程序的代码可以在本仓库中找到:github.com/PacktPublishing/Vue.js-3-for-Beginners。如果你不知道什么是仓库或如何使用它,我建议你学习基础知识,即使每个章节都会提供使用代码所需的所有信息和命令。

仓库为每个章节有多个分支。这将是每个章节的起点,并在每个章节的开始处指定,如前所述。

仓库的主要分支是最新提交,其中包含完整的应用程序。如果你有时间,我建议你运行完整的应用程序来尝试浏览,看看我们在本书的进程中会实现什么。

要运行应用,你可以简单地遵循项目根目录中可用的README.md文件中的说明。

由于这是我们第一次运行应用,我还会提供这里所需的信息,以便让应用启动并运行:

  1. 首先,我们需要在我们的机器上获取远程仓库的副本。为此,请在终端中运行以下命令:

    git clone https://github.com/PacktPublishing/Vue.js-3-for-Beginners
    
  2. 然后,我们需要导航到我们新创建的项目文件夹:

    cd vue-for-beginners
    
  3. 在我们能够运行项目之前,我们需要使用包管理器安装所有依赖项。包管理器是一种用于安装和管理包的软件,在我们的案例中,是 Node.js 和 JS,项目依赖于它们。存储库中共享的应用支持所有主要的包管理器,如 npm、yarn 和 pnpm。在以下示例中,我们将使用 npm:

    npm install
    
  4. 最后,是时候运行项目了。以下命令将运行项目的开发版本:

    npm run serve
    

几秒钟后,应用的本地实例将启动,你应该可以通过在浏览器中打开HTTP://localhost:5173来访问它。应用应该看起来像这样。

图 1.1:伴随应用仪表板的截图

图 1.1:伴随应用仪表板的截图

在应用中花些时间进行导航,既在浏览器中,也在代码库中,看看我们在本书的过程中将构建什么。

在本节中,我们学习了伴随应用,它是如何支持我们的学习的,其核心功能,以及最后,在本地运行应用所需的命令。在下一节中,我们将花几分钟时间讨论网络应用的核心领域,并解释我们将使用哪些技术/库在我们的应用中。

网络应用的核心领域

JS 生态系统并不羞于使用框架和库,但即使有如此广泛的选择,它们大多共享相同的核心理念和领域。这些是网络应用的核心部分,无论你使用哪个框架来编写你的应用,你都必须了解这一点,并对其含义有一个基本的理解。

网络应用的基础如下:

  • 用户界面(UI):这指的是用户可以与之交互的屏幕上的元素。简单来说,你可以在互联网上看到或与之交互的任何东西都是 UI 的一部分。网络开发的核心领域通常使用基本的 HTML/CSS、纯 JS(这是另一种说纯 JS 的方式),或者像 React、Vue 或 Angular 这样的框架来实现。在我们的案例中,这将使用 Vue.js 3 来实现。

  • fetch方法。

  • 状态管理:除非你的网站是一个静态博客文章,否则你需要处理一些数据。这可能是表单的当前状态或登录用户的详细信息。小型应用程序可以轻松地直接使用框架提供的现有工具来实现这一点,但有时这需要扩展到使用完整的“状态管理”。在 Vue.js 中,有两个主要的库可以帮助你处理你的数据。Vuex 是 Vue 2 的首选状态管理库,而 Pinia 是 Vue 3 建议使用的库(Pinia 是 Vuex 的一个更新版本,但由于它经历了一次全面的重写并带来了许多破坏性变化,因此被重新命名)。由于我们将使用 Vue 3 编写我们的 Companion App,我们将使用 Pinia。

  • 路由:尽管按照定义,今天的大多数网站都被称为单页应用(SPAs),但实际上它们使用了不止一个页面。对“单页”的定义只是因为应用程序在导航期间不会完全重新加载,但这并不意味着应用程序不会有超过一个的路由。因此,大多数 Web 应用程序将需要一个处理多个页面之间路由的方法。在本书的范围内,我们将使用 vue-router,这是官方的路由库。

  • 表单和验证:表单可能是 JS 框架和单页应用(SPAs)之所以如此成功的主要原因之一。无需刷新页面即可处理复杂表单和客户端交互的可能性,极大地提升了用户体验(UX)。即使 Vue.js 完全能够处理表单及其验证,我们仍将使用一个名为 VeeValidate 的外部库来进行客户端验证。

  • 调试:构建并不总是直截了当的,调试应用程序是一项必备技能。尽管这并不是应用程序的真正部分(因为它更多的是一项技能,而不是应用程序本身的实际部分),但我仍希望将其包括在 Web 应用程序的核心领域之一,因为调试有助于我们使应用程序更加安全和高效。在我们的案例中,我们将使用纯 JS 技术和一个名为 Vue.js devtools 的浏览器扩展来帮助我们分析、研究和改进我们的应用程序。

在本节中,我们解释了构成 Web 应用程序的不同区域。我们还解释了将在我们的 Companion App 中使用的技术栈背后的架构决策。现在是时候了解一个名为组件化架构的基本方法论了。这是大多数前端框架的基础。

组件化架构

我们已经到达了介绍性讨论的最后一部分,我们几乎准备好开始编码了。本节将介绍组件化架构的概念。即使你已经熟悉这个主题,我也建议你继续阅读本章,因为它将支持我们在本书后续部分将做出的某些决策。

在本节中,我们将介绍在引入这个概念之前网络开发是如何工作的,然后我们将讨论组件化架构是如何塑造我们今天所了解的网络开发行业的。

一次一个页面

如果你像我一样做了这么多年的开发者,你可能已经使用过那些在页面定义和开发方面不够灵活的语言和框架。几年前使用.NET 和 PHP 意味着每个网页都是使用单个文件创建的(免责声明:一些语言有“部分”的定义)。

这个方法一直有效,直到 JavaScript 在前端开始使用并动摇了生态系统。JavaScript 将网站从静态页面变成了非常动态的实体,并在这样做的同时推动了需要更动态的工具,而这些工具与之前的发展工具不兼容。

让我们考虑一个标准的网站主页,例如以下这个:

图 1.2:标准主页的线框图

图 1.2:标准主页的线框图

这个网站遵循一个标准的布局,包括头部和底部,横幅,一些特色内容,以及一个联系我们表单。想象一下,所有这些都在一个单独的 HTML 文件中。在那些日子里,这可能是所有内容都放在一个单独的 HTML 文件中,例如一个共享的样式表文件,比如 CSS(层叠样式表)文件。如前所述,行业中的事情开始发生变化,JavaScript 的使用越来越多。

在先前的场景中,JavaScript 可能只是用来添加一些基本的交互性,比如横幅中的幻灯片动画,产品列表中的花哨滚动,或者在联系我们表单中处理表单提交。

简而言之,这个变化逐渐将行业引导向前端库和框架。这些库和框架旨在帮助管理和简化在 JavaScript 中产生的数百行代码,它们通过引入组件化架构来实现这一点。

将事物分解成小的单元在行业中并不是什么新鲜事,因为后端框架已经通过使用面向对象编程有了这个概念,但在行业的前端方面是一个创新。

从一个页面到多个组件

组件化开发(CBD)是一种模式,其中给定应用程序的 UI 被分解成多个“组件”。将大页面分解成小的独立单元可以减少应用程序的复杂性,并帮助开发者专注于每个部分的个别范围和责任。

所有当今的前端框架都是基于这个模式构建的,而今天的前端开发是由基于 CBD(组件化开发)的架构驱动的。

让我们看看之前的主页示例,看看我们如何将其拆分成小的独立组件。

主页将被分解成以下组件:

  • 头部:一个将包括标志和用于显示账户信息(如头像)的逻辑的组件

  • 幻灯片:一个可重用的组件,用于显示幻灯片图像

  • 特色:用于显示特色文章的组件

  • 联系我们:一个包含所有验证和提交我们表单所需逻辑的组件

  • 页脚:一个静态组件,将包含一些文本和社交媒体链接

图 1.3:分为不同部分(如标题、幻灯片、特色、联系我们和页脚)的仪表板线框图

图 1.3:分为不同部分(如标题、幻灯片、特色、联系我们和页脚)的仪表板线框图

如我们将在几分钟内看到的,图 1.3 中显示的组件只是一个示例,因为一个完全定义的 CBD 实际上会将事物分解得更细,直至单个 HTML 元素。这意味着不仅页面由组件组成,组件本身也由更小的组件组成。

将事物分解成更小的单元有许多好处。让我们分析一些这些特性:

  • 可重用性:CBD 为您提供了创建可在应用程序内部重用组件的可能性。(在我们的例子中,我们可以重用标题、页脚、幻灯片,甚至特色组件。)

  • 封装:封装被定义为每个组件能够“自包含”。所有样式、HTML 和 JS 逻辑都被“封装”在给定组件的作用域内。

  • 独立性:由于封装,每个组件都是独立的,并且不与其他组件共享(或不应共享)责任。这允许组件在不同的上下文中使用(例如,在网站的不同页面上使用功能组件的能力)。

  • 可扩展性:由于组件是“自包含”和独立的,我们能够以有限的风险对其进行扩展。

  • 可替换性:组件可以轻松地与其他组件或移除,而不会带来风险。

从前面的列表中可以看出,使用 CBD 为前端开发者带来了许多好处。正如我们将在本书的学习过程中体验到的,将应用程序分解成小单元的能力对于新开发者来说极为有益,因为它允许将各个主题分解开来,并真正关注最重要的方面。

Vue.js 使用 .vue 扩展实现基于组件的架构,并将样式、HTML 和逻辑(JS 或 TypeScript)封装在同一文件中。SFC 将在本书稍后详细介绍。

原子设计

在本节的最后,我们将了解在整个书籍过程中我们将如何构建我们的组件。

组件的文件夹结构在行业内尚未标准化,这可能会因开发者而异。

在我们的案例中,我们将遵循业界所知的“原子设计”。这被描述为:

布拉德·弗罗斯特(Brad Frost,https://bradfrost.com/)创建的原子设计方法是一种设计方法,用于构建具有明确顺序和层次结构的设计系统 blog.kamathrohan.com.

原子设计模式遵循在化学和物质组成中描述的相同概念。如果您想深入了解这个主题,我建议您阅读以下文章:blog.kamathrohan.com/atomic-design-methodology-for-building-design-systems-f912cf714f53

在这本书中,我们将遵循该方法中提出的方法论层次结构,将我们的应用程序分解为“亚原子”、“原子”、“分子”、“有机体”、“模板”和“页面”。

图 1.4:原子设计提供的不同级别的视觉解释(来源:https://blog.kamathrohan.com/atomic-design-methodology-for-building-design-systems-f912cf714f53)

图 1.4:原子设计提供的不同级别的视觉解释(来源:blog.kamathrohan.com/atomic-design-methodology-for-building-design-systems-f912cf714f53

原子设计层如下:

  • 亚原子层: 亚原子层包括将在应用程序中使用的所有变量和设置。这些不是“组件”,而是将在我们的应用程序中全局共享的 CSS 变量。在亚原子层中,我们可以找到颜色、字体和间距。

  • 原子: 这些是定义单个 HTML 元素的组件,例如,按钮、图标和输入文本都属于原子。

  • 分子: 分子由两个或更多原子或纯 HTML 元素组成。例如,带有标签和错误的输入字段就是一个分子。

  • 有机体: 有一些 UI 组件组成一个可以用于网站的独立部分。例如,登录表单是一个有机体,幻灯片也是一个有机体,页脚也是如此。

  • 模板: 在前端生态系统中,这些通常被称为布局,用于定义多个页面使用的可重用结构。一个例子可能是一个包含英雄图片、侧边栏、主要内容区域和页脚的模板。这个模板将在应用程序的许多页面中使用,将其抽象成自己的模板可以减少重复。

  • 页面: 最后,我们有页面。这些用于定义我们的 Web 应用程序页面或子页面。页面将是我们的数据加载的地方,它将包括 HTML 元素、有机体、分子和原子。

即使从前面的描述中理解这种分离可能看起来很复杂,我们将在本书中多次涉及这个话题,这将有助于您理解可用的层之间的主要区别。

花些时间查看应用程序的文件夹结构,并阅读组件的名称,以尝试理解我们将如何拆分我们的应用程序。

关注点分离

到目前为止,我们已经了解到现代框架提供了将应用程序拆分成称为组件的小块的能力,并且组件本身也存在层次结构。

在本节中,我们将简要介绍为什么最初引入这种层次结构,并了解这将如何帮助我们进行开发。

原子设计不仅帮助我们根据视觉复杂度拆分组件,还帮助我们拆分应用程序逻辑,以创建高性能和可扩展的应用程序。

随着组件定义的复杂度增加,与之关联的预期逻辑也变得更加复杂。

图 1.5:每个层级的 UI 和逻辑复杂度示意图

图 1.5:每个层级的 UI 和逻辑复杂度示意图

我们所说的逻辑复杂度是什么意思?逻辑复杂度可以描述为组件正常工作所需的 JS 数量。

例如,逻辑复杂度低的组件,如按钮,将拥有非常有限的 JS,而更复杂的组件,如输入字段,则需要处理字段验证和错误放置;此外,页面还需要负责从 API 加载数据,格式化数据以确保其处于正确的格式,并处理其子组件的可见性。

在本节中,我们介绍了如何使用组件化架构来构建应用程序。我们介绍了构成组件库的不同层级,并最终定义了当与 Vue.js 等前端框架结合使用时,这种方法的优势。现在,让我们在总结部分回顾本章内容。

总结

现在是本章的结尾,在这个阶段,你应该已经了解了我们在这本书中将要实现的内容以及我们将使用的方法。

我们已经了解了我们的伴侣应用程序以及它将包含的内容。我们简要介绍了章节的结构以及它们将如何支持你的学习之旅,并最终介绍了重要主题,如组件化架构、原子设计和构成任何前端项目基础的前端开发的核心领域。

在下一章中,我们将开始学习 Vue.js 的基础和其核心原理,并开始为我们的伴侣应用程序赋予生命。

第二章:Vue.js 的基础

如果你正在阅读这本书,那么你很可能已经决定将 Vue.js 作为你的首选框架,而且几乎没有理由试图说服你不要使用它。我们将利用这一章来开始分享 Vue.js 独特之处及其成功的原因。

我们首先将学习是什么让 Vue.js 与其他框架不同;然后我们将继续研究 Vue.js 的响应性和其生命周期。最后,我们将了解 Vue.js 的组件结构。

在本章中,我们将学习以下内容:

  • Vue.js 的响应式基础

  • 理解 Vue.js 的生命周期和钩子

  • Vue.js 的组件结构

本章的目标是提供有关 Vue.js 的信息,这将成为你未来学习的基石。理解 Vue.js 的响应性将帮助你区分 Vue.js 与其他前端框架和库,你将学习 Vue.js 组件的完整生命周期,以帮助你做出正确的技术决策。最后,了解定义 Vue.js 组件的不同方式将为你准备接下来的章节。

Vue.js 的响应式基础

Vue.js 已经存在一段时间了;该框架的第一个版本可以追溯到 2014 年,当时其创造者、前谷歌开发者 Evang You 通知了全世界其创建的消息。

埃文在谷歌的 Angular 经验为他构建一个优秀的框架提供了必要的知识。在框架公开后不久,埃文在接受 Between the Wires 采访时说了以下内容:

我想,如果我能提取我真正喜欢 Angular 的部分并构建一个真正轻量级的框架会怎样。

埃文不仅创建了一个轻量级的框架,而且还成功构建了一个围绕它的令人惊叹的社区,使其成为开发者最喜爱的框架之一。

到目前为止,Vue.js 只发布了三个主要版本,最新的一个版本是完全重写,使 Vue.js 更快、更小,甚至更容易使用。

两个主要方面使 Vue.js 非常成功。第一个方面是它的增长和采用是由社区为社区所驱动的。Vue.js 是少数几个没有大公司支持的顶级框架之一。它完全由对核心团队的捐赠者资助,其发展主要是由社区推动的。这从 Vue.js 生态系统内始终存在的关注开发体验中可以看出。

第二个使其独特的方面是其响应式系统。Vue.js 的核心引擎被构建为在幕后具有响应性,这使得使用 Vue.js 处理状态变得简单直观。

当我们谈论开发中的响应式时,我们指的是某些变量在发生变化时自动更新的能力。一个简单的响应式例子是 Excel 和 Google Sheets,设置一个计算,比如一列的总和,会导致总和“响应”任何在求和单元格中发生的变化:

图 2.1:Google 表格示例,展示单元格值如何自动更新

图 2.1:Google 表格示例,展示单元格值如何自动更新

与 Excel 类似,在 Web 开发中,尤其是在 Vue.js 中,响应式允许你的变量在依赖的值发生变化时动态更新。

让我们通过一个真实示例来了解响应式在 UI 框架中扮演的重要角色。让我们先看看纯 JavaScript 的行为,然后看看这如何转化为 Vue.js。

在下面的代码中,我们将创建两个变量firstNamelastName,然后我们将尝试创建一个名为fullName的响应式变量:

let firstName = "Simone";
let secondName = "Cuomo";
const fullName = `${firstName} ${secondname}`;
console.log(fullName);
// output: Simone Cuomo

在前述代码片段中,控制台打印的全名等于我们创建的firstNamesecondName变量。如果我们现在将firstName变量更改为不同的值,会发生什么?fullName的值会是什么?

let firstName = "Simone";
let secondName = "Cuomo";
const fullName = `${firstName} ${secondname}`;
console.log(fullName);
// output: Simone Cuomo
firstName = "John";
Console.log(fullName);
// output: Simone Cuomo

如前述代码所示,本应打印全名的变量输出了错误值,因为它没有对firstName变量的变化做出“响应”。

这种行为是完全可以理解的;你不想在 JavaScript 中让所有变量都自动响应,因为这会使它的使用变得复杂,但当涉及到 UI 时,有更新的值是预期的行为。

例如,假设你正在装满一个篮子,并且当你向篮子里再放一个物品时,你希望物品的总数发生变化,或者当你在一个受限的文本框中输入时,你希望字数统计更新,等等。

让我们使用 Vue.js 复制前述示例:

let firstName = ref("Simone");
let secondName = ref("Cuomo");
const fullName = computed( () => `${firstName.value} ${secondName.value}`);
console.log(fullName);
// output: Simone Cuomo
firstName.value = "John";
Console.log(fullName);
// output: John Cuomo

在这个阶段,你对 Vue.js 的理解仍然局限于你在阅读这本书之前的经验,所以你还不期望理解前述代码。我们需要关注的是代码产生的输出与纯 JavaScript 产生的输出之间的比较。

如前述代码所示,fullName变量在其依赖变量(firstNamesecondName)发生变化时,会自动更新。

理解响应式系统背后的工作原理超出了本书的范围,但这并不妨碍我们理解幕后发生的细节。

下面的图解展示了幕后发生的事情以及响应式实际上是如何发生的:

图 2.2:解释 Vue.js 响应式系统的图解

图 2.2:解释 Vue.js 响应式系统的图解

这幅插图是 Vue.js 响应性核心系统中发生的事情的简化版本,以使我们的变量动态化。让我们分析一下正在发生的事情:

  1. 我们定义了响应式变量,例如 FirstName。Vue.js 会监视这个变量的任何变更事件。

  2. 我们声明了一个依赖于其他响应式变量的复杂变量(例如,fullName)。

  3. Vue.js 跟踪一个依赖树。它创建了一个列表,列出了什么依赖于什么。

  4. 当一个响应式变量发生变化时,响应性引擎将触发 onDependenciesChange

  5. Vue.js 评估哪些值依赖于刚刚更改的值,并且只有在该值是它们的依赖项之一时才会触发更新。

在阅读前面的过程时,您可能认为这听起来很熟悉,您是对的,因为响应性系统遵循与 HTML 元素(如输入字段)提供的相同原则。例如 <input><select> 以及许多其他元素都有能力持有值,并在它们被更改时通过触发 onChange 或类似事件来做出响应。

如前例所示,监听一个变更事件来处理数据响应性并不是什么独特的事情。那么,是什么让 Vue.js 的响应性系统变得特别?Vue.js 的响应性在处理依赖树和自动更新后台变量方面脱颖而出。Vue.js 的响应性系统是非侵入性的,并且对开发者来说完全不可见。Vue.js 在其 生命周期 中管理所有后台依赖,并以高速和高性能对变化做出反应。

本节向您介绍了 Vue.js 的响应性系统,解释了它在框架成功中扮演的重要角色。然后,我们通过一些示例解释了后台引擎的工作原理。现在,我们需要通过更仔细地查看其生命周期并了解它们如何在我们的应用程序开发中使用来理解 Vue.js 核心引擎的工作方式。

理解 Vue.js 的生命周期和钩子

随着我们深入本书,我们对 Vue.js 的了解不断扩展。在本节中,我们将讨论 Vue.js 的生命周期。

当我们使用 Vue.js 时,应用程序会经过一系列定义的步骤,从创建组件 HTML 到收集所有动态值,以及将这些值显示在 DOM 中。这些都是我们所说的 生命周期 的一部分,在本节中,我们将定义它们所有,并学习在开发生涯中何时以及如何使用它们。

如果您以前尝试学习过 Vue.js,您可能已经接触到了以下图表,该图表可在 Vue.js 的主要文档中找到:

图 2.3:Vue.js 生命周期图(来自 www.vuejs.org)

图 2.3:Vue.js 生命周期图(来自 www.vuejs.org)

无论你使用 Vue.js 有多长时间,前面的图表都会反复出现在你的浏览器历史记录中,并且它会慢慢地印在你的记忆中,因为它是 Vue.js 的基础,并且为了编写干净和高效的代码,这是必须了解的。

随着你在这本书的学习过程中不断进步,你将被要求回顾生命周期的不同部分,并且你将被要求回顾这个图表。

在下一节中,我们将逐步回顾这个图表,并理解它的含义以及如何在开发过程中应用这些知识。

我们将从顶部开始解释,但我们将从beforeCreate开始。我们故意将setup留到后面,因为介绍完所有生命周期后,它更容易理解,即使它是列表的第一部分。

以下的生命周期是逐步的;这意味着其中一个生命周期的结束状态是下一个生命周期的开始。

beforeCreate

这个生命周期在组件初始化时创建。在这个阶段,我们的组件根本不存在。Vue.js 框架刚刚被指示创建它,并且正在触发这个钩子来通知我们组件正在创建中。

在这个阶段,组件没有任何可用内容,没有创建 HTML,也没有设置任何内部变量。

通常,这个生命周期用于触发分析日志或长时间异步任务。

created

在这个阶段,Vue.js 已经知道你的组件,并且已经加载了其 JavaScript 逻辑,但它还没有渲染或挂载任何 HTML。

这正是触发异步调用以收集数据的完美阶段。现在触发一个慢速请求将帮助我们节省一些时间,因为这个请求将在我们的组件渲染的同时在幕后继续进行。

beforeMount

这个生命周期在 HTML 被附加到 DOM 之前触发。这个生命周期的用例非常有限,因为大多数预渲染操作都是在created生命周期中触发的。

mounted

在这个阶段,组件已经被完全渲染,其 HTML 已经被附加到 DOM 上。如果你需要执行任何需要访问 DOM 的操作,这就是正确的生命周期,因为 HTML 已经准备好被读取和修改。

如果你来自非框架背景,你可能认为你组件的大部分逻辑可能都包含在这个生命周期中,但你很快就会学到,由于 Vue.js 组件的指定方式,你很少需要访问 DOM。

beforeUpdate 和 update

beforeUpdateupdate形成一个递归循环,每当组件数据或依赖发生变化时都会发生。我们已经在上一节中介绍了这一步骤,当时我们讨论了响应式系统。

beforeUpdate在 Vue.js 意识到组件依赖的响应式值发生变化时触发。

另一方面,update 在值完全改变并被分配到正确的 DOM 节点并准备就绪时触发。

你很少需要直接使用这两个生命周期,因为 Vue.js 提供了其他功能,例如计算属性和观察者,以便能够处理组件数据中的单个更改。

beforeUnmount 和 unmount

在这个阶段,我们的组件不再需要,Vue.js 准备将其从 DOM 中移除。这可能是由于用户导航到不同的页面或任何其他需要从 UI 中移除组件的事件。

beforeUnmountunmount 在使用上几乎没有区别。这个生命周期对于取消订阅事件非常有用,例如“点击”和“观察者”,如果保持激活状态,可能会导致性能下降。

setup

正如本节开头所承诺的,我们将 setup 放在最后,因为所有生命周期都被介绍之后,它更容易解释。setup 本身不是一个生命周期,但它是由 CompositionAPI(你将在本章稍后了解一些)使用的入口点。当使用 setup 时,你可以调用和访问所有生命周期(mountedupdatedunmounted 等)。你可以将 setup 视为 Vue.js 生命周期的包装器,一个包含所有生命周期钩子(hooks)的单个方法。Composition API 将是我们在这本书中使用的,我们将在稍后的阶段更详细地解释 setup 函数。

在本节中,我们学习了 Vue.js 的基本流程,介绍了所有其生命周期。在这个阶段,我们应该知道 Vue.js 组件何时渲染、更新或销毁。这些知识将推动我们的开发,并允许我们做出正确的选择,使我们的应用程序性能更优。在下一节中,我们将看到如何引入 Vue.js 组件语法,我们还将学习如何利用前面的生命周期。

Vue.js 组件结构

组件是 Vue.js 框架的基础。它们是使用此框架创建应用程序所需的构建块。正如之前所解释的,组件可以小到简单的按钮,也可以大到整个页面。

无论大小如何,所有组件都使用相同的语法和结构构建。在本节中,我们将学习编写组件的不同语法形式,并了解构成 Vue.js 单文件 组件SFC)的不同部分。

单文件组件

SFCs 是 Vue.js 特有的,可以在具有 .vue 扩展名的 Vue.js 项目中找到。这些文件由三个主要部分组成:templatescriptstyle

<template></template>
<script></script>
<style></style>

Vue.js 编译器在构建时将前面的三个部分拆分成单独的块。我们现在将在这个部分中解释每个部分。我们将按照以下顺序介绍 SFC 部分:

  1. 模板

  2. 样式

  3. 脚本

<template>标签

第一个部分是<template>。这个部分包括我们组件托管 HTML。所以,如果我们以一个极其简单的按钮为例,模板将看起来像这样:

<template>
    <button>My button</button>
</template>

与 React 不同,Vue.js 组件的 HTML 是纯HTML,而不是JSX。正如我们在本书的学习过程中将要了解到的,Vue.js 提供了一些实用的工具来简化我们的 HTML 内容。

重要提示

至于模板编写样式,可以使用不同的方法来编写 HTML,例如使用渲染函数,或者通过在 JSX(使用正确的加载器)中编写它,但这些两种方法仅适用于特定用途,并且不期望在 Vue.js 生态系统中使用。

<style>标签

下一个可用的部分是<style>。这个部分将包括与我们的组件相关的样式,使用组件中的普通<style>标签不会将样式作用域化到特定的组件。

在我们继续之前,让我们解释一下样式作用域的实际含义以及如何在我们的 Vue.js 应用中实现这一点。

当我们使用简单的<style>标签,如前例所示,我们的样式将泄漏到应用的其余部分。除非我们使用 CSS 进行作用域化,否则我们在标签中声明的任何内容都将全局有效:

<style>
p {
  color: red;
}
</style>

在 Vue.js 中编写前面的样式是允许的,并且出于性能和可维护性的原因甚至被建议。问题是前面的声明将改变整个应用中段落的颜色为红色,而不仅仅是它被编写的组件中的颜色。

幸运的是,Vue.js 有一个实用的工具可以在我们希望我们的组件完全作用域化时使用,确保没有样式泄漏并破坏应用的其余部分。要做到这一点,我们需要在我们的<style>标签中添加一个名为scoped的属性:

<style scoped>
p {
  color: red;
}
</style>

使用这个新属性,我们的样式将被锁定到定义它们的组件中,并且不会影响应用的其余部分。我们将学习在构建我们的伴侣应用时何时最好使用这两种方法。

<script>标签

在 SFC 中可用的下一个部分是<script>标签。这个标签将包含组件的 JavaScript 逻辑,从组件接受的属性到用于定义组件逻辑的私有数据,再到组件正常工作所需的实际方法。

只是在几年前,当 Vue.js 的主要版本仍然是 2 时,组件大多使用一种称为选项 API的语法来定义。尽管有其他方法可用,但这仍然是编写 Vue.js 组件的主要方式。

随着 Vue.js 3 的发布,创建组件的新方法被引入。这种方法与现有的选项 API 并存,并提供了更好的 TypeScript 支持,改进了逻辑复用的技术,以及灵活的代码组织。这种方法被称为组合 API

重要提示

组合 API 也被称为脚本设置

在当前这个时间点,两种方法都没有被官方明确地推荐为优于另一种;这一点也被 Vue.js 官方文档所强调,它目前展示了所有使用这两种方法的教程和示例,并提供了在方法之间切换的选项:

图 2.4:Vue.js 官方文档中的 API 偏好切换

图 2.4:Vue.js 官方文档中的 API 偏好切换

本书及其配套应用的内容将使用 Composition API 编写。这个决定基于两个主要原因:

  • 由于 Vue.js 2 的历史原因,网络上充斥着专注于 Options API 的资源,而对新的 Composition API 语法的关注较少

  • Evan You(Vue.js 的创造者)已经(不止一次)预测,从长远来看,Composition API 将接管并成为标准

因为我是坚信额外知识无害的强烈支持者,所以在本节中,我们将学习如何使用两种语法定义组件,包括 Options API。了解这两种方法可以帮助你建立一个强大的基础,以支持你对 Vue.js 框架的学习。

Options API 与 Composition API——同一枚硬币的两面

在底层,这两种方法实际上会产生非常相似的结果,Composition API 生成的代码性能略高。尽管如此,这些方法带来的语法差异和好处是相当不同的,并且会根据你的习惯产生重大变化。

Composition API 与 Options API 之间的第一个和主要区别在于代码中<script>部分的分解方式。正如我之前提到的,这两种语法将提供相同的功能,这意味着我们可以声明 props 数据、计算和函数,以及访问两种方法中的所有生命周期,但我们实现的方式不同。

差异如下:

  • Composition API:按功能分解的代码

  • Options API:按 Vue.js 选项分解的代码

让我们通过一个例子来清楚地定义这两种方法之间的差异。

图 2.5:分解代码的 Composition API 与 Options API 比较

图 2.5:分解代码的 Composition API 与 Options API 比较

如前图所示,在 Options API 中,代码并没有考虑实际的组件需求和逻辑,而是通过 Vue.js 选项:PropsDataMethodsComputedmounted等垂直切割。

另一方面,Composition API 通过按组件的技术输出分解组件,采取不同的方法。这允许我们为功能 1 创建一个部分,为功能 2 创建一个部分,依此类推。

第二个区别与 TypeScript 支持有关。这是 Vue.js 核心团队决定在 Vue.js 3 重写期间创建组合 API 的主要原因。Options API 提供了非常基本的 TypeScript 支持,这阻止了许多开发者加入 Vue.js 生态系统。

我们已经到达了这个部分的结尾,现在是时候清楚地说明哪种方法更好了,但不幸的是,答案是这取决于。

由于两种语法糖都在相同的代码中编译,所以这个决定实际上回到了编码偏好。Options API 提供了更多的结构,因此在你创建组件的经验仍然有限时,它可以在你的职业生涯初期提供更多帮助,而组合 API,由于增加了 TypeScript 支持和代码分区更大的灵活性,可以成为提高大型应用程序代码可读性的强大工具。

样本组件

在这个阶段,我们已经对 Vue.js 的基础知识有了足够的了解,可以准备介绍一些样本组件并看到框架的实际应用。

我们将查看一个原子的例子。在我们的情况下,它是一个简单的图标组件。这个样本组件将展示以下功能:

  • 它将接受几个属性(sizename

  • 它将包含一些样式

  • 它将动态加载图标

该组件将使用以下 HTML 调用:

<vfb-icon name="clog" size="small" @click="doSomething />

如前所述,在这个部分,我将展示使用两种编写方法的组件;然而,在本书的后面,我们只会使用脚本设置(组合 API)编写组件。

重要提示

请注意,我们将在本书的后面更详细地介绍所有这些内容。这只是一个对 Vue.js 组件的快速介绍。

使用 Options API 的原子组件

让我们先看看这个组件作为 Vue.js 组件使用 Options API 时的样子:

<template>
  <img
    :src="img/iconPath"
    :class="sizeClass"
  />
</template>
<script>
  export default {
    name: 'vfb-icon',
    props: {
      size: String,
      name: String
    },
    computed: {
      iconPath() {
        return `/assets/${this.name}.svg`;
      },
      sizeClass() {
        return `${this.size}-icon`
      }
    }
};
</script>
<style scoped>
.small-icon {
    width: 16px;
    height: 16px;
}
.medium-icon {
  width: 32px;
  height: 32px;
}
.large-icon {
  width: 48px;
  height: 48px;
}
</style>

现在我们来分解所有部分,从<template>开始,它包含我们组件的 HTML;在这种情况下,这是一个原生的<img>元素。这个组件有几个属性传递给它。前两个是原生属性:srcclass

:src="img/iconPath"
:class="sizeClass"

这些属性与你在 HTML 中习惯的声明方式略有不同,因为它们前面有一个:。当一个属性有这种语法时,这意味着它的值是动态的,并且值(在我们的情况下,iconPathsizeClass)将被评估为 JavaScript 变量,而不是实际的字符串。

重要提示

请注意,你可以在 Vue.js 中编写纯 HTML,使用动态变量不是框架的要求,而只是一个特性,使属性动态化。

接下来,让我们转向应用程序的逻辑部分,即<script>部分。在这里,我们首先声明我们组件的名称:

name: 'vfb-icon',

重要提示

对于所有 Vue.js 组件来说,由两个单词组成是一个好的实践。这将确保组件不会与原生 HTML 元素冲突。

接下来,是时候声明属性了。属性是在组件初始化时被组件接受的值。这在开发中是一个现有概念,因为所有 HTML 元素都接受诸如classidvalue之类的属性。这些属性使我们的组件可重用且灵活。在我们的例子中,我们声明了两个不同的属性:namesize。这些属性在组件被调用时会被传递下去,就像它们是原生的 HTML 属性一样:

props: {
  size: String,
  name: String
},

这个例子展示了一个属性的简单配置,我们只是定义了它的类型,但正如我们将在本书的后续内容中看到的,props可以有不同的配置,例如验证、默认值和需求规则,以确定它们是否是必需的。

下一段代码是我们声明动态属性的地方。为了我们的组件能够正确运行,我们需要定义一个路径和一个类,分别命名为iconPathsizeClass。这些值将是动态的,因为它们包含了动态属性,并且将使用所谓的计算属性来声明:

computed: {
  iconPath() { return `/assets/${this.name}.svg`; },
  sizeClass() { return `${this.size}-icon` }
},

计算属性允许我们声明响应式值(记得书中的响应式章节),并且可以利用我们组件的全部逻辑;在这种情况下,我们只使用了 props,但我们可以使用一系列值和外部逻辑来创建一个新的值。

重要提示

请注意,当使用 Options API 时,你必须使用this关键字来访问组件内的变量,例如propscomputed。在我们的例子中,我们使用它通过this.namethis.size来访问属性。

我们最后一个部分是<style>

<style scoped>
  .small-icon {
    width: 16px;
    height: 16px;
  }
  .medium-icon {
    width: 32px;
    height: 32px;
  }
  .large-icon {
    width: 48px;
    height: 48px;
  }
</style>

这相当简单,因为这个例子没有包含比在纯 CSS 中通常看到的任何不同之处。正如前一章中提到的,我们可以添加作用域为我们的样式的属性,以确保它们的样式不会从我们的组件中溢出。

在前面的例子中,你可以看到(在实践中)Options API 是如何将我们的组件划分为各个部分的。在我们的例子中,我们有propsnamecomputedmethods

export default {
  name: '',
  props: {},
  computed: { },
  methods: { }
}

使用 Script Setup 的 Atom 组件

现在是时候看看使用<script setup>语法编写的相同组件了:

<template>...</template>
<script setup>
  import { computed } from 'vue';
  const props = defineProps({
    size: String,
    name: String
  });
  const iconPath = computed( () => {
    return `/assets/${props.name}.svg`;
  });
  const sizeClass = computed( () => {
    return `${props.size}-icon`;
  });
</script>
<style scoped>...</style>

如你清晰可见,前面的例子省略了<template><style>标签。这些标签被省略是因为它们与 Options API 的对应部分相同。事实上,正如我们之前提到的,这两种方法之间的区别仅影响组件的逻辑部分,即<script>

我们组件的第一行是import

import { computed } from 'vue';

与 Options API 不同,在<script setup>中使用时,我们必须从'vue'导入每个单独的 Vue.js 方法,就像我们在之前的代码中为computed所做的那样。

接下来,我们将看到如何在 Composition API 中定义属性:

const props = defineProps({
  size: String,
  name: String
})

属性是使用 <script setup> 时唯一几个可以具有详细声明的选项之一。实际上,为了能够声明它们,我们需要使用一个名为 defineProps 的编译器宏。宏不需要导入,因为它们只是将被编译器使用并在代码中删除。如果你曾经使用过 TypeScript,你将熟悉这种方法。

接下来,我们将学习 computed

  const iconPath = computed( () => {
    return `/assets/${props.name}.svg`;
  });
  const sizeClass = computed( () => {
    return `${props.size}-icon`;
  });

声明 computed 的属性与 Options API 非常相似,但有两个小的不同点:

  • computed 属性的逻辑需要作为回调传递给从 'vue' 导入的计算方法

  • this 关键字不再可用,我们可以直接访问变量

至此,我们将对 Options API 和 Composition API 之间的差异进行解释。我们将在本书的后面部分更详细地介绍 Script Setup(Composition API)。如果你对 Vue.js 非常陌生,本节可能包含了很多新的语法,理解起来可能有些困难,但一旦我们开始构建我们的伴侣应用,随着你对 Vue.js 及其语法的了解加深,事情会迅速变得更有意义。

在本节中,我们开始学习如何定义 Vue.js 组件以及构成 SFC 的不同部分。然后,我们通过详细覆盖一个示例组件来结束本节。

摘要

我们现在已经到达了本章的结尾,这是为了我们开始应用构建过程所必需的。在本章中,我们通过分析其响应性系统,学习了 Vue.js 与其他框架的不同之处。然后,我们分解了 Vue.js SFC 的组成,也称为 .vue 文件,并通过分析框架中所有不同的生命周期钩子,走过了 Vue.js 组件的生命周期。

在本章的中间部分,我们通过使用示例组件来探索它们,学习了 Composition API 和 Options API 之间的主要差异。

在下一章中,我们将通过开始构建我们的伴侣应用(Companion App)来学习 Vue.js。这将是你从一名完全的初学者成长为一名经验丰富的 Vue.js 开发者的漫长旅程的开始。

第二部分:理解 Vue.js 的核心功能

本书第二部分专注于 Vue.js 及其核心功能。我们将一步一步地构建伴侣应用的核心,同时继续扩展我们对 Vue.js 框架的了解。

本部分包含以下章节:

  • 第三章**,使我们的 HTML 动态化

  • 第四章**,利用 Vue 内置指令实现轻松开发

  • 第五章**,在 Vue.js 中利用计算属性和方法

  • 第六章**,Vue.js 中的事件和数据处理

  • 第七章**, 使用 Vue.js 处理 API 数据和异步组件管理

第三章:使我们的 HTML 动态化

理论章节现已结束。是时候开始构建我们的伴侣应用程序并学习 Vue.js 了。随着我们一步一步地构建应用程序,我们也将学习 Vue.js。这种通过实践学习的方法,当你跟随并与我一起构建应用程序时,效果最为显著。

为了帮助您掌握复杂主题并确保您已经掌握了 Vue.js 的基础知识,您还将被要求完成一些额外任务,这些任务可以应用于伴侣应用程序或作为独立项目使用。

在本章中,我们将涵盖以下主题:

  • 构建你的第一个 Vue 项目

  • 创建我们的第一个组件

  • 介绍属性

  • 了解 Vue.js 的 Refs 和响应式数据

本章的目标是向您介绍 Vue.js 的基础知识。您将学习如何从头创建项目,以了解 Vue 组件的结构。在本章结束时,您将能够创建 Vue 组件,使用 props 定义属性,并使用 Refs 和响应式处理私有状态。

技术要求

从现在开始,所有章节都将要求您从我们的仓库中检出特定版本的代码。我们是在第一章中安装我们的仓库,当时我们下载了应用程序并首次运行它。

使用相同的仓库,该仓库可在 github.com/PacktPublishing/Vue.js-3-for-Beginners 找到,我们可以使用各种分支在章节之间跳转。每个章节都有一个单独的分支,确保我们的起点一致,防止可能出现的问题或缺失的部分,这会使学习变得复杂。

在本章中,分支被命名为 CH03。要检出此分支,请运行以下命令或使用您选择的 GUI 来支持此操作:

git switch CH03.

请记住,在切换分支后运行应用程序之前,您必须确保所有依赖项都已安装并运行开发服务器。这可以通过以下两个命令实现:

npm install
npm run dev

请注意,我们不需要仓库,直到到达 创建我们的第一个 组件 部分。

构建你的第一个 Vue.js 项目

是时候开始构建我们的伴侣应用程序了。如果您已经熟悉 Vue.js 以及如何使用它创建新项目,您可以跳过本节,从我们的仓库中拉取代码,并在下一章开始构建应用程序。

当我们使用 vue create 命令创建新项目时,我们将很快看到,我们使用的是 Vite 构建工具。直到最近,最好的构建工具是 Webpack,包括 Vue 2 在内的所有框架都使用它来构建他们的应用程序。但现在情况已经改变,Vite 由于其无配置方法和极快的开发服务器而接管了市场。

在其官方网站上,Vite 被描述如下:

“Vite(法语中意为“快速”,发音为/vit/,类似于“veet”)是一个旨在为现代网页提供更快、更精简的开发体验的构建工具”

Vite 是由 Evan You(是的,Vue 的作者)创建的,旨在改善开发体验。Vite 仅仅存在了几年,但由于其低配置和快速的开发服务器,它已经迅速获得了人气。

与 Vue.js 一样,Vite 是完全开源的,并且它也支持所有主流框架。使用 Vite 创建项目相当简单,你只需要一个 IDE 和一个终端。

Vue CLI 图形界面

你可能听说过 Vue CLI 提供了一个可视化工具,帮助你管理 Vue 应用程序。不幸的是,该项目与 Webpack 相关,并且尚未导入 Vue 3 和 Vite。

创建 Vue 命令

由于无法在现有文件夹中创建项目,我们无法重用之前下载的应用程序。我们将在这个不同的文件夹中完成这一步,以确保你能够从头开始创建项目。在下一章中,我们将直接从配套应用程序仓库中拉取代码。

要使用 Vue 创建新项目,我们首先需要访问项目将要创建的文件夹。请放心,CLI 将为你的项目创建一个新的文件夹,所以你现在不需要手动创建文件夹,只需访问项目应该存在的父文件夹即可。例如,我喜欢在我的“文档”文件夹中创建所有项目,所以我将这样访问它:

// Mac users
cd ~/Documents/
// Windows users
cd %USERPROFILE%/documents

现在我们已经处于正确的文件夹中,我们可以调用创建新 Vite 项目所需的终端命令:

npm create vue@latest

执行前面的命令将生成一个安装 create-vue 包的请求:

图 3.1:由 create vue 命令触发的安装信息

图 3.1:由 create vue 命令触发的安装信息

要成功安装项目,我们需要按 y 并继续安装。

几秒钟后,CLI 将启动并询问一些问题,这些问题将帮助它根据我们的需求搭建项目:

图 3.2:Vue CLI 的问题

图 3.2:Vue CLI 的问题

如你所见,Vue 项目附带了一套预设,有助于为你的下一个 Vue 项目打下坚实的基础。Vue CLI 提供了以下设置的选项:

  • 项目名称

  • TypeScript

  • JSX 支持

  • 路由

  • 状态管理

  • 单元测试

  • 结束 单元测试

  • 代码质量

  • 代码格式化

这些设置的选项完全取决于你,你应该遵循你个人的需求和要求。你在 图 3.2 中看到的设置是我用来创建下一章我们将使用的配套应用程序的设置。

在按下 Enter 并等待几秒钟后,我们应该会得到一些有关如何运行我们的项目的信息。这需要我们访问文件夹,安装所需的包,并运行开发服务器。

首先,让我们导航到作为我们的 Vue 项目初始化一部分创建的文件夹,即等于你的项目名称:

cd "vue-for-beginners"

然后,安装项目运行所需的所有包。在这个例子中,我使用了 npm,但你也可以使用 Yarn 或 PNPM:

npm install

最后,我们只需运行这个命令来运行开发服务器:

npm run dev

在执行这两个命令后,不到一秒钟你应该会在你的控制台中看到以下消息:

图 3.3:Vue 开发环境成功运行时的 Vite 输出

图 3.3:Vue 开发环境成功运行时的 Vite 输出

Vite 项目与之前在 3000 端口运行的 Webpack 项目表现不同,它们运行在 5173 端口上。本地 URL 将在控制台中显示,如图 图 3.3 所示。

在我们的情况下,访问 localhost:5173 上的浏览器将显示以下网站:

图 3.4:新构建的 Vue 项目的欢迎页面

图 3.4:新构建的 Vue 项目的欢迎页面

恭喜你创建了你的第一个 Vue 项目。这将是你众多项目中的第一个。

Vue 项目文件夹结构

在本节中,我们将快速介绍新 Vue 项目的结构。

当创建一个新项目时,它自带一个定义良好的结构,可以作为未来开发的坚实基础:

图 3.5:新创建的 Vue 项目的文件夹结构

图 3.5:新创建的 Vue 项目的文件夹结构

我们将解释不同的文件夹和文件,帮助你从你的新 Vue 项目中找到所需的一切。我们将按无特定顺序进行。

根目录

Vue 项目的根目录包含一些配置文件。这些文件是由 Vue 创建包预设和预生成的,并且为了使应用程序顺利运行,不需要进一步的关注。在本书的进程和你的职业生涯中,你将逐渐接触到这些配置文件的每一个,并了解它们的各种选项。

不可触碰区域

有几个文件夹,如 .vscodenode_modulesdist,是我所说的“不可触碰”文件夹。你可能已经熟悉这些文件夹,因为它们是由你可能已经使用的工具和软件创建和管理的,例如 Visual Studio Code、npm 或 Vite,并且不应手动修改。

Public

在项目构建后,public 文件夹的内容将被直接复制到输出文件夹中。这个文件夹很少被开发者触及,但在需要包含在构建输出中但不是 Vue 编译部分的文件时非常有用。这个文件夹的示例文件包括 favicon 和 service worker。

Cypress

如安装指南所示,新创建的项目附带了一个预设的 端到端E2E)测试框架,使用您选择的工具。在我们的例子中,我选择了 Cypress,CLI 已经为我创建了一个文件夹和一个示例测试,供我使用。

SRC

这就是您的源代码所在的地方。这是我们应用程序的主要内容,您将在日常工作中的大部分时间在这里工作。由于文件夹的重要性,我们将查看其内容并确保我们知道其文件的结构:

图 3.6:新创建的 Vue 项目的 SRC 文件夹内容

图 3.6:新创建的 Vue 项目的 SRC 文件夹内容

如前所述,让我们从文件夹的根目录开始。这包括两个文件,main.jsApp.vueMain.js 是我们应用程序的入口文件。此文件用于向我们的 Vue 实例添加新包,并加载和设置全局插件、组合式(利用 Vue 的组合式 API 封装和重用我们在本书后面将要介绍的有状态逻辑的函数)和组件。接下来,我们有 App.vue。这是第一个 Vue 入口点,是负责加载和处理 Vue 应用程序其余部分的组件。

接下来,我们有 assets 文件夹。此文件夹用于加载任何资产,例如图片、PDF 和视频。此文件夹的内容也会复制到我们的构建输出工件中。

在列表的下方,我们有 components 文件夹。此文件夹不仅包含应用程序中已提供的组件集合,还包括 __tests__ 文件夹,其中包含我们的单元测试。

接下来的两个文件夹是 routerstores。正如其名所示,它们分别包含 vue-routerPinia 存储代码。这两个是 Vue.js 核心团队提供的核心包,将在 第十章第十一章 中详细介绍。Vue-router 将用于为我们的客户端创建导航路由,并帮助我们管理不断增长的应用程序,而 Pinia 将用于在应用程序中创建和管理数据。

最后但同样重要的是,我们有 views 文件夹。如果你有时间调查这个文件夹,你会注意到它包含简单的 Vue 组件。这个文件夹的原因是为了将简单的组件单元(存储在 components 文件夹中的那些)与实际的路由页面分开。这种分离有助于保持代码整洁并界定应用程序的路由。

您的私人游乐场

即使我们所创建的应用程序对于本书的其余部分不是必需的,但它可能作为练习本书过程中将要涵盖的主题的游乐场而有用。

我们已经完成了 Vue 项目的解释,你现在应该具备从头创建 Vue 项目的知识。你也应该对 Vite 有一些了解,并且能够导航新创建的 Vue 项目的文件夹结构。在下一节中,我们将深入代码并开始构建我们的第一个 Vue.js 组件。

创建我们的第一个组件

如您从第一章中记得的,我们在其中介绍了伴随应用程序,我们计划构建一个名为 X(前身为 Twitter)的社会应用程序的克隆。为了以优雅的方式开始我们的构建之旅,我们将构建应用程序中最标志性的组件,一个帖子。

图 3.7:X.com 帖子组件的示例

图 3.7:X.com 帖子组件的示例

在本章中,我们将学习如何在书库的不同分支之间切换。然后我们将创建我们的第一个 SFC (RefsReactive)。

创建 Post.vue

回到 第一章,我们提到我们将把组件分解成不同的层级(原子、分子、生物体等),而 SocialPost.vue 将成为分子层的一部分。

因此,让我们在 component 文件夹中创建一个名为 molecules 的文件夹,然后添加一个名为 SocialPost.vue 的文件。一旦完成这些,你的文件夹应该看起来像这样:

图 3.8:伴随应用程序的 src 文件夹的文件树

图 3.8:伴随应用程序的 src 文件夹的文件树

关于我们创建的新文件,有两点需要注意:

  • 名称由两个单词组成。这不仅仅是为了提供更多上下文,还因为像 post.vue 这样的单字母组件是不被推荐的,因为它们可能与未来的原生 HTML 组件(如 <button><table>)发生冲突(例如,如果未来 HTML 版本中引入了一个名为 的新 HTML 元素,它可能会与我们的自定义组件冲突)。

  • 组件名称以 PascalCase 编写,这是一种命名约定,其中变量组成的每个单词的首字母都大写。

由于我们的文件是空的,让我们打开它,并通过添加 <template><script><style> 标签来创建 Vue 组件的基本结构:

<template></template>
<script setup ></script>
<style lang="scss"></style>

这将是我们标准的 Vue 启动模板。它定义了一个空模板,我们将在这里封装我们的 setup 属性,这允许我们使用组合式 API 编写 JavaScript 逻辑,以及一个 style 部分,在其中我们选择 SCSS 作为我们的预处理器。

现在,我们将定义用于显示我们的帖子所需的 HTML 和 CSS。目前这将是一个非常简单的设计;我们将在本书的后续内容中添加更多。

我们组件的第一个草稿将包括一个头部图片,用户的名称和用户 ID,以及帖子的描述。所有这些也将包括一些基本样式。让我们看看代码:

<template>
<div class="SocialPost">
  <div class="header">
    <img class="avatar" src="img/40" />
    <div class="name">Name of User</div>
    <div class="userId">@userId</div>
  </div>
  <div class="post">This is a dummy post</div>
</div>
</template>
<script setup >
</script>
<style lang="scss">
.SocialPost{
  .header {
    display: flex;
    align-items: center;
    margin-bottom: 8px;;
  }
  .avatar {
    border-radius: 50%;
    margin-right: 12px;
  }
  .name {
    font-weight: bold;
    margin-right: 8px;
    color: white;
   }
}
</style>

如你很可能注意到的,前面的组件没有什么特别之处。没有 script 标签,HTML 中没有特殊的标签,CSS 中也没有什么特殊之处,但它仍然是一个完全正常的 Vue 组件。

让我们突出这个组件的几个重要方面:

  • <div class="SocialPost">:将类名设置为组件名,在我们的例子中是 SocialPost,分配给组件的根元素是一个好习惯。这将帮助我们限制样式范围,而无需使用 scoped 属性。

  • <style lang="scss">:在我们的示例中,我们将使用 SCSS。这在这里指定了。正如你将在下一节中看到的那样,这需要在我们的 Vite 项目中进行配置。你不需要添加预处理器,但我添加了一个,以便向你展示如何添加和使用它,以防你习惯于使用预处理器编写样式。

  • .SocialPost{:我们可以使用附加到组件名的类来限制我们的 CSS,通过将所有样式包装在它里面。这将确保我们的样式不会渗透到其他组件中。

现在组件已经准备好了,是时候测试它了。为了做到这一点,我们需要在应用程序的某个地方加载组件。我们可以在 TheWelcome.vue 中加载组件。

要成功加载 Vue 自定义组件,我们需要完成两个简单的操作。首先,我们需要导入组件,其次,我们需要在 HTML 中调用它,就像它是原生组件一样。

要加载组件,我们需要像导入普通 JavaScript 文件一样导入它:

<script setup>
  import SocialPost from './molecules/SocialPost.vue'
</script>

现在组件已经加载,我们可以在 HTML 中简单地这样使用它:

<template>
  <SocialPost></SocialPost>
</template>

现在我们已经完全开发并加载了组件,是时候尝试它了。为了做到这一点,让我们使用 npm run dev 命令运行我们的应用程序。现在,访问终端中显示的本地站(http://localhost:5173/)。

不幸的是,浏览器输出并不是我们预期的;我们遇到了一个错误:

图 3.9:Vite 显示的错误信息

图 3.9:Vite 显示的错误信息

幸运的是,这个错误是预期的。正如我之前提到的,SASS 要求我们进行进一步的配置。我想向你展示如果配置错误,Vite 会如何反应。如错误信息所示,Vite 注意到我们正在使用 SASS,并且还提供了安装它的命令。所以,让我们在终端中运行这个命令:

npm install –D sass

运行此命令并刷新浏览器后,我们的应用程序现在应该会显示我们的组件:

图 3.10:Vite 欢迎屏幕显示新创建的自定义组件

图 3.10:Vite 欢迎屏幕显示新创建的自定义组件

恭喜!你刚刚编写了你的第一个工作 Vue 组件。这只是一个小步骤,但庆祝每一个成就都很重要。

如您从我们刚刚创建的文件中注意到的,Vue.js 允许您编写由 HTML 和 CSS 组成的简单组件。这是使用您现有的开发知识慢慢开始使用 Vue 的好方法。

您的回合

尝试添加另一个您自己的组件,例如,以确保您已经理解了组件是如何创建和添加的。您应该尝试创建一个静态页脚为我们应用程序。

在本节中,我们学习了如何创建和加载 Vue 组件,我们解决了 Vite 的第一个问题,并学习了如何安装新插件,最后,我们通过包裹其 CSS 找到了一种新的方法来将样式范围限定到我们的组件。在下一节中,我们将通过介绍一个称为属性的功能来学习如何使我们的组件变得动态。

介绍属性

如您所注意到的,我们在上一节中创建的组件是静态的,不能在实际应用中使用,因为它总是显示相同的信息,而不是实际的帖子。

在接下来的部分,我们将向我们的组件添加一些动态功能。为了确保每个主题都能被充分理解,我们将在每个部分添加一个小功能,并确保我们在整本书的过程中有足够的时间来重复这些功能。

在本节中,我们将通过公开属性来改变我们的帖子组件的结构。属性仅仅是组件暴露的属性,允许用户自定义其行为或样式。

如果您曾经使用过 HTML,您可能已经对 Vue.js 的 props 很熟悉了。许多原生 HTML 元素都有用于修改组件的属性,例如,将 <input> 标签的 Type 属性用作改变其外观,将 <textarea> 标签的 columnrows 属性用作改变其外观,以及将 <img> 标签的 src 属性用作定义其图像的 URL。

Vue.js 属性(通常称为 props)允许我们在组件中定义这个属性,使我们能够将我们的静态组件转变为动态和灵活的构建块。

在本节中,我们将选择我们之前创建的帖子组件,并公开一些 Vue.js 属性,以便我们可以使用不同的值多次使用它。

在重新审视我们的帖子组件后,很明显以下变量应该被更改为动态条目:

  • Username: Twitter 用户名

  • UserId: Twitter ID

  • AvatarSrc: 头像图片的来源

  • Post: 帖子的内容

在 Vue.js 中声明 props

我们使用 props 的第一步是在组件中声明它们。声明属性意味着定义其名称和类型。为此,我们可以使用 defineProps 编译器宏:

<script setup >
  const props = defineProps({
    username: String,
    userId: String,
    avatarSrc: String,
    post: String
  });
</script>

如此所示,defineProps 宏接受一个包含我们属性的对象。在我们的例子中,这些都是 String 类型,但其他类型,如 NumberObjectArrayBoolean,也是接受的。

当我们声明一个属性时,我们通知组件及其用户,这个组件愿意接受这些额外的数据。

现在是时候学习如何在我们的 SFC 中访问这些属性了。

在 Vue.js SFC 中访问属性

属性可以在多个地方访问。它们可以直接在 HTML 中以播放字符串的形式读取,它们可以在 HTML 元素声明中使用,或者它们可以在script标签中作为我们组件逻辑的一部分使用。

所有这些方法都有不同的语法,但即使看起来很多,由于它与 Vue 的编写风格一致,所以将很容易记住。

首先,我们将学习如何将属性用作纯文本。这是通过使用两个大括号{{ props name }}来完成的。将此应用于我们的模板将生成以下代码:

<template>
<div class="SocialPost">
  <div class="header">
    <img class="avatar" src="img/40" />
    <div class="name">{{ username }}</div>
    <div class="userId">{{ userId }}</div>
  </div>
  <div class="post">{{ post }}</div>
</div>
</template>

如您所见,usernameuserIdpost的值不再是硬编码的,现在它们正在使用底层的属性。

接下来,我们将学习如何在模板中使用变量。我使用了通用的单词变量而不是属性,因为这个概念适用于所有变量,而不仅仅是属性。要在模板中使用动态值,例如作为 HTML 元素属性,我们只需要在属性前加上符号:。所以,在我们的场景中,图像属性src="img/..."将变成:src="img/avatarSrc"

我们的<img>元素看起来像这样:

<img class="avatar" :src="img/avatarSrc" />

在属性前加上:告诉 Vue 该值不是一个普通的字符串,而是一个实际的 JavaScript 变量。所以,在接下来的示例中,类被评估为一个字符串,但src的值不会是字面上的avatarSrc,而是与该名称关联的 JavaScript 变量。

最后,我们将学习如何在script标签中访问属性。这是通过使用defineProps的返回值来实现的。

让我们通过尝试在组件挂载时记录username的值来将我们刚刚学到的知识付诸实践。代码应该看起来像这样:

<script setup >
import { onMounted } from 'vue';
const props = defineProps({
  username: String,
  userId: Number,
  avatarSrc: String,
  post: String
});
onMounted( () => {
  console.log(props.username);
});
</script>

上一段代码展示了如何使用defineProps来访问属性。这个函数接受一个属性对象(在我们的例子中是usernameuserIdavatarSrcpost),并将返回一个变量,该变量包含在组件初始化时传递的所有响应式属性(例如<MyComponent username="simone" />)。接下来,我们介绍了 Vue.js 的另一个新特性onMounted。它在第二章中作为 Vue 生命周期的一部分被引入。OnMounted专门在组件完全渲染到页面上时触发。

defineProps不能被解构

defineProps返回的值不能被解构。解构返回的对象会导致非响应式值。

由于我们移除了硬编码的字符串,并将组件改为使用属性,在我们能够在浏览器中测试它之前,我们需要做一步额外的工作。就像接受属性的 HTML 元素一样,我们需要在创建组件实例时定义我们的属性,在我们的例子中是在TheWelcome.vue中发生的。

让我们看看如何更新我们的组件以包含我们新创建的属性:

<template>
  <SocialPost
    username="Username one"
    userId="usernameID1"
    avatarSrc="https://i.pravatar.cc/40"
    post="This is my post"
  ></SocialPost>
</template>

就像普通的 HTML 元素一样,我们能够直接将属性传递到 HTML 标签中。这里使用的属性名与组件中定义的属性名相同。它们不仅需要逐字匹配,而且还需要区分大小写。

现在我们已经完全更新了组件,我们可以通过运行 CLI 命令来启动开发服务器(npm run dev)并检查浏览器(http://localhost:5173/)来访问我们的应用程序。

我们的应用程序应该看起来与组件的前一个版本没有区别。实际上,我们大部分的工作是改变组件在幕后如何表现,而不是它的外观。本节包含了多个主题和 Vue 特性。让我们回顾一下到目前为止我们学到了什么:

  • 如何声明 props

  • 如何使用 props

  • 如何将 props 用作普通字符串

  • 如何将 props 用作 HTML 属性

  • 如何在 script 标签中使用 props

  • 如何使用我们的第一个 Vue 生命周期,onMounted

记住,Vue.js 中的属性就像 HTML 属性一样。它们允许你通过暴露可以以任何形式使用的值来使组件动态化,从而使组件具有独特性。这些属性可以在组件的多个部分中访问。

在下一节中,我们将学习如何创建一个或多个组件实例,并介绍一个新概念:响应式数据。

你的回合

继续扩展你在第一部分创建的页脚组件,但使用 props 将值改为动态的。

使用 Refs 和响应式学习 Vue.js 的响应式数据

在上一节中,我们开始使我们的组件动态化,但这只是使组件完全可重用的两个步骤中的第一个。现在是时候学习组件状态了,也称为数据(在 Option API 中使用术语)或 Refs 和响应式(在组合 API 中使用术语)。能够设置私有组件信息,以及定义组件属性的能力,将是我们构建动态和灵活组件的工具集。

在我们跳入数据之前,我们需要回到上一节,看看我们刚刚创建的组件。如果你仔细看,SocialPost.vue的版本看起来很相似,并且似乎硬编码版本和动态版本之间没有实际的区别。

那么,为什么我们费尽心机做出所有这些改变,而实际上没有什么改变呢?嗯,改变是存在的,我们只是还没有使用它。

让我们稍微思考一下我们的伴侣应用,并尝试理解SocialPost组件将如何被使用。当使用真实的社交平台时,我们永远不会期望屏幕上只有一个硬编码的帖子;我们的时间线最终将显示大量动态帖子。在我们组件的第一个版本中,即包含硬编码值的版本,多次创建组件只会导致相同的作者和标题反复显示。但有了我们创建的动态版本,我们有传递不同值给 props 的机会,从而允许我们创建多个独特的帖子。让我们通过创建第二个帖子来看看这将在实践中看起来如何:

<SocialPost
  username="Username one"
  userId="usernameID1"
  avatarSrc="https://i.pravatar.cc/40"
  post="This is my post"
></SocialPost>
<SocialPost
  username="Username two"
  userId="usernameID2"
  avatarSrc="https://i.pravatar.cc/40"
  post="This is my second post"
></SocialPost>

创建动态组件是 Web 开发中的一个非常强大的工具。它允许我们重用相同的组件并简化我们的开发工作。即使使组件动态化比我们最初的硬编码示例前进了一步,但它仍然需要一些改进。是时候学习响应式数据以及它如何帮助我们简化组件的 HTML 了。

将逻辑与 HTML 分离

一个干净的组件是指其大部分逻辑都封装在<script>标签内,并且具有非常干净的 HTML。虽然将一些逻辑添加到 HTML 中可能很有吸引力,但这会导致组件难以维护。

将 Refs 或 Reactive 添加到我们的组件中,使我们能够从 SFC 的<template>部分中移除静态数据,并使我们的代码具有一些动态性。

Refs 和 Reactive 的定义可以是一组原始数据、对象和数组,这些数据由组件实例使用来定义私有响应式数据(状态)。”

这个概念并不新鲜。事实上,原生的 HTML 组件也持有它们自己的状态。例如,视频组件可能持有开始或停止的状态,而下拉菜单可能持有其选中的值或展开或折叠的内部状态。

在 Vue.js 和其他主要框架中,响应式数据不仅用于声明状态(例如,保持侧边栏的当前状态为打开或关闭),还用于存储组件内部使用的组件数据,以提供特定功能。

在我们的案例中,我们将使用私有数据尝试将单个帖子的信息移动到数组中。这将允许我们在未来的章节中使用外部工具,如 API,来动态获取这些数据。

在我们深入实际实现细节之前,让我们定义一下 Refs 和 Reactive 之间的区别:

  • Refs:允许声明原始值,如字符串、数字和布尔值,以及更复杂的数据类型,如数组和对象

  • Reactive:允许声明对象和数组,但不能用于原始值

图 3.11:支持的和不支持的 Refs 和 Reactive 类型表

图 3.11:支持的和不支持的 Refs 和 Reactive 类型表

使用对象

一些开发者喜欢将 Refs 用于所有事情,而另一些开发者则喜欢根据分配的类型来分割使用。我将要展示的可能是主观的,你可以自由地根据你的偏好更改你的使用方式。

在本书的整个过程中,我们将使用 Refs 来处理原始值,如字符串、数字和布尔值,以及使用 Reactive 来处理数组和对象。

Refs 和 Reactive 之间的主要区别不仅在于它们可以持有的值,还在于它们的使用方式。我们将对我们的组件进行两次修改,以更好地理解 Refs 和 Reactive 之间的区别。

首先,我们将通过修改 SocialPost 组件并为其添加一个新功能来介绍 Refs。然后,我们将通过将帖子信息(userIdavatarnamepost)移动到一个数组中来简化我们的 HTML,来学习关于 Reactive 的知识。

将 Refs 添加到 SocialPost.vue

能够为组件定义私有数据是非常强大的。我们已经看到组件可能需要通过定义属性从其父组件接收信息,但有时组件需要处理自己的状态。在本节中,我们将通过提供选择能力来对我们的组件 SocialPost.vue 进行一些修改。

为了实现这个功能,我们需要对我们的组件进行三个修改:

  • 我们将创建一个名为 selected 的私有变量

  • 当组件被选中时,我们将为其分配一个特定的样式

  • 当组件被点击时,我们将修改 selected 的值

让我们从创建第一个私有变量开始。如前所述,这将使用 ref 来完成。这是 Vue 库提供的一个方法,它接受一个用于初始化的值。例如,如果我想为我的名字生成一个变量,我会写 const name = ref("Simone")。在我们的情况下,selected 将是一个布尔值,并且它将被初始化为 false,因为组件在首次渲染时预期不会被选中:

<script setup >
import { onMounted, ref } from 'vue';
const selected = ref(false);
const props = defineProps({
…

如前述代码片段所示,声明 ref 非常简单。首先,我们从 Vue 中导入它,然后我们可以通过传递变量的初始值来调用它。组件的其余部分被省略,但与前几节相同。

接下来,我们为 selected 状态创建一个样式,并找到一种方法,当 selected 的值发生变化时动态地添加这个样式。让我们首先创建一个新的类 SocialPost__selected,并在这个类激活时添加一个白色边框:

<style lang="scss">
.SocialPost{
  &__selected{
    border: white solid 1px;
  }
  .header { ...

我们将把新的样式添加到SocialPost.vue中。多亏了 SCSS 的帮助,&辅助在&__selected中。如果你以前从未见过这种语法,这是一个 SASS 特性,它将自动将&替换为父声明的名称。因此,在我们的情况下,__selected将被.SocialPost前缀,创建.SocialPost__selected。SASS 不是必需的,你可以使用纯 CSS 实现这些样式,但我决定添加它来展示 Vue 与 Vite 的灵活性,并帮助你体验真实应用可能利用的功能。

为了使选中的帖子突出,我们只需在组件周围声明一个白色边框。

现在是时候将这个类分配给我们的组件了,但我们要根据selected的值动态地完成这个任务。我们的代码将看起来像这样:

<template>
  <div
    class="SocialPost"
    :class="{ SocialPost__selected: selected}"
  >
  <div class="header">
  ...

我们刚刚介绍了 Vue.js 的一个新特性。实际上,使用纯 HTML 无法动态分配类,但 Vue 正好为我们提供了这个特性。

在 Vue.js SFC 中访问属性中,我们提到,在属性前加上:允许我们提供动态值,在class属性的情况下,允许我们分配一个或多个动态类。

:class属性接受一个对象,如果其值为真,则应用于特定的类。因此,在我们的情况下,如果selected的值为true,它将分配一个名为SocialPost__selected的类。

我们现在已经准备好进行最后一步,这是我们的组件增强的最后部分,它将使我们能够切换组件并显示其选中状态。

到目前为止,我们已经创建了一个特定的样式并声明了一个存储我们状态的变量。剩下要做的就是当组件被点击时修改我们的状态变量selected。我们将通过在组件的根目录中使用@click属性来完成这项工作:

<template>
  <div
    class="SocialPost"
    :class="{ SocialPost__selected: selected}"
    @click="selected = !selected"
  >
  <div class="header">...

通过使用原生的@click事件处理程序和一些基本的 JavaScript,我们能够修改我们的selected变量并更新我们的组件状态。

如果你对这个语法不熟悉,通过编写selected = !selected,我们正在将selected的值更改为当前值的相反。所以如果当前值是true,它将设置为false,反之亦然。

如果我们运行我们的应用程序并点击其中一个组件,我们应该看到以下结果:

图 3.12:Companion App 显示两个帖子,其中一个处于选中状态,带有白色边框。

图 3.12:Companion App 显示两个帖子,其中一个处于选中状态,带有白色边框。

我们现在已经学会了如何声明和使用 Refs来定义组件状态。在下一节中,我们将继续学习父组件TheWelcome.vue,并学习如何使用响应式

使用响应式来托管我们的帖子信息

保持干净的 HTML 是可维护应用程序的关键,因此在本节中,我们将使用 Reactive 来改进 TheWelcome.vue。我们将声明一个 array 类型的私有变量。正如我们上面提到的,我们将使用 Reactive 来声明和管理数组:

<script setup>
  import { reactive } from 'vue';
  import SocialPost from './molecules/SocialPost.vue'
  const posts = reactive([]);
</script>

使用 Reactive 的方式与 Ref 非常相似,因为它需要从 Vue 库中导入并使用一个基本值进行初始化。在我们的例子中,我们给我们的变量命名为 posts

在前面的代码中,分配的值是一个空数组,但我们需要将其更改为包含当前在 HTML 中持有的实际帖子值。我们的 Reactive 初始化将更改为以下内容:

<script setup>
import { reactive } from 'vue';
import SocialPost from './molecules/SocialPost.vue'
const posts = reactive([
  {
    username: "Username one",
    userId: "usernameId1",
    avatarSrc: "https://i.pravatar.cc/40",
    post: "This is my post"
  },
  {
    username: "Username two",
    userId: "usernameId2",
    avatarSrc: "https://i.pravatar.cc/40",
    post: "This is my second post"
  }
]);
</script>

现在我们已经准备好了变量,是时候更改 HTML 的内容以使用我们的 Reactive 值了。就像属性和 Refs 一样,我们可以在 HTML 中直接使用它。

我们将通过使用 posts[0].usernameposts[0].avatar 等方式访问第一篇帖子的信息。就像我们之前做的那样,我们将通过在前面加上 : 来通知 Vue.js 我们的 props 值是动态的。组件应该看起来像这样:

<template>
  <SocialPost
    :username="posts[0].username"
    :userId="posts[0].userId"
    :avatarSrc="posts[0].avatar"
    :post="posts[0].post"
  ></SocialPost>
  <SocialPost
    :username="posts[1].username"
    :userId="posts[1].userId"
    :avatarSrc="posts[1].avatar"
    :post="posts[1].post"
  ></SocialPost>
</template>
<script setup>
  import { reactive } from 'vue';
  import SocialPost from './molecules/SocialPost.vue'
  const posts = reactive([
    {
      username: "Username one",
      userId: "usernameId1",
      avatar: "https://i.pravatar.cc/40",
      post: "This is my post"
    },
    {
      username: "Username two",
      userId: "usernameId2",
      avatar: "https://i.pravatar.cc/40",
      post: "This is my second post"
    }
  ]);
</script>

我们现在已经从模板中清理了硬编码的值,它已经被使用 Reactive 声明的动态值所替代。使用 Refs 和 Reactive 定义组件将是您整个职业生涯中 Vue 组件的基础。

摘要

本章向您介绍了 Vue 的一些基本功能,并为我们伴侣应用程序定义了第一个组件。我们通过学习如何使用 CLI 创建 Vue 应用程序并查看其文件夹结构开始了本章。然后我们创建了我们的第一个 Vue 组件。通过这样做,我们学习了如何使用 SFC 语法编写和使用组件。然后我们将我们的静态组件更改为使用动态属性。最后,我们学习了组件状态,并通过增强组件的功能学习了如何使用 Refs 和 Reactive 数据。

轮到你了

在另一个组件中使用 Ref 和 Reactive 的概念。这可以通过将之前创建的 footer.vue 文件中的 link 值和 src 移动到 Reactive 属性中来实现,就像我们为帖子所做的那样。

在下一章中,我们将继续学习 Vue 的使命,通过介绍 Vue 指令。指令是 Vue 特有的属性,它赋予我们使用简单代码满足复杂需求的能力。我们将首先介绍指令的概念,然后创建新的组件或更新现有的组件来了解 Vue 框架中可用的不同内置指令。

第四章:利用 Vue 内置指令轻松开发

在上一章中,我们看到了 Vue.js 的特性,如动态类、属性和私有状态,如何帮助我们简化开发。在本章中,我们将继续探讨我们之前的话题,通过向您介绍 Vue.js 内置指令的世界来构建。这些指令是 Vue.js 的第一次真正尝试,并将为你提供使你的代码更具动态性的工具。这些指令的主要目标是简化你的工作流程并使你的开发更加容易。在整个章节中,我们将介绍两个新的组件 TheHeader.vueSocialPostComments.vue,同时继续对现有的组件如 SocialPost.vue 进行工作,使其更具动态性和为未来的章节做好准备。

我们将把本章分成三个不同的部分:

  • 使用 v-textv-html 显示文本

  • 使用 v-ifv-show 处理元素可见性

  • 使用 v-for 简化模板

到本章结束时,你将基本了解一些 Vue.js 内置指令,并能够使用 v-ifv-show 切换组件的可见性,并学习如何通过使用 v-for 来简化你的 HTML 模板,以及使用 v-textv-html 渲染文本或 HTML。

技术要求

完成本章所需的代码可以在 GitHub 上的一个名为 CH04 的分支中找到。要拉取此分支,请运行以下命令或使用您选择的 GUI 来支持您进行此操作:

Git switch CH04

不要忘记,仓库可以在 github.com/PacktPublishing/Vue.js-3-for-Beginners 找到。

与上一章的结尾相比,分支有一些小的变化。这些变化是为了简化书籍,让我们能够专注于学习材料,而不是花太多时间在设置事情上。

使用 v-text 和 v-html 显示文本

HTML 是一个非常强大的工具,但它确实有其局限性,这迫使我们编写非常冗长且难以维护的代码。如果你曾经在没有框架的完全静态网站上工作过,你可能遇到过包含许多重复代码块的冗长 HTML 页面。这就是 Vue 指令发挥作用的地方。

Vue 的指令在 Vue School 网站上被描述为“特殊的 HTML 属性,允许我们操作 文档对象模型 (DOM)。”

在之前的章节中,我们看到了 Vue 如何使用现有的属性语法添加功能,如组件属性。Vue 指令采用类似的方法,通过使用类似于现有 HTML 的语法来创建新的功能。

之前的描述说 Vue.js 指令允许我们操作 DOM,但有没有任何原生的 HTML 属性做类似的事情?答案是肯定的。

HTML 为我们提供了诸如<input>元素中的“value”这样的属性,用于分配可见值。我们还在<textarea>中有“rows”和“columns”,它们定义了元素的大小。这个列表可以很长,但我认为展示指令可以帮助我们实现什么是有益的。

Vue 的内置指令,允许我们通过使用元素属性来修改 DOM 并增强组件的灵活性。

以下是 Vue 的 15 个内置指令:

  • v-text: 设置给定元素的innerText

  • v-html: 设置给定元素的innerHTML

  • v-show: 切换元素的可见性(display: hidden

  • v-if: 当条件满足时渲染元素

  • v-else: 如果前面的条件(v-ifv-else-if)未满足,则显示元素

  • v-else-if: 如果前面的条件(v-if)未满足,并且当前条件得到满足,则显示元素。

  • v-for: 根据源数据多次渲染元素或模板块

  • v-on: 当事件被触发时触发回调

  • v-bind: 将一个或多个属性或组件属性动态绑定到表达式

  • v-model: 在表单输入元素或组件上创建双向绑定

  • v-slot: 定义一个由父组件使用并替换为任意标记的占位符

  • v-pre: 跳过此元素及其所有子元素的编译(通常用于显示代码块)

  • v-once: 只渲染元素和组件一次,并跳过未来的更新

  • v-memo: 缓存模板的子树

  • v-cloak: 用于在模板准备好之前隐藏未编译的模板

一些前面的指令,包括v-memov-oncev-cloak,是针对更高级用户,本书的讲解过程中不会涉及,但如果您感兴趣,可以在 Vue.js 官方文档中找到更多关于这些信息(vuejs.org/api/built-in-directives.html#v-once)。其余的指令将会有解释,并伴随代码示例。

值得了解

所有内置指令都以v-为前缀。这样做是为了能够将框架属性与 HTML 中可用的原生属性分开。此标准还用于创建应始终以相同前缀开始的自定义指令。

让我们从我们的第一个指令v-text开始。

使用 v-text 将动态文本应用到我们的组件上

Vue.js 指令是 Vue.js 功能的一个非常强大的补充。不仅因为它们提供了额外的功能,而且因为它们有助于编写更干净、更易读的代码。

当被问及代码可读性有多重要时,我总是引导人们阅读罗伯特·马丁的《Clean Code》一书,他在书中指出以下内容:

“实际上,阅读与写作所花费的时间比例远远超过 10 比 1……[因此,]使其易于阅读使得写作更容易。”

让我们开始使用一些这些指令,看看它们如何被集成到我们的伴侣应用中。

在本章中,我们将工作于一个新的组件,该组件将被用作我们伴侣应用的标题。这个组件已经在仓库中创建,您可以在 components/organisms/ 文件夹中找到它。

如您从保存组件的文件夹中推断出的那样,这个组件将成为我们之前在 第一章 中讨论的原子设计结构中的“有机体”。

在我们继续之前,让我们解释一下为什么我们的组件将被命名为 TheHeader.vue。Vue 组件的文件名需要始终唯一且与原生 HTML 元素不同,这阻止了我们简单地将其命名为 header,因为它将与原生的 HTML header 元素冲突。为了避免这些问题,Vue.js 组件名称应该始终由两个单词组成,使用 Pascal 大写格式编写,其中第一个单词和任何附加单词以大写字母开头。在某些情况下,找到两个单词来组成组件名称可能很困难。当这种情况发生时,我们可以用单词 the 作为组件的前缀。使用这种方法为我们的标题组件生成组件名称为 TheHeader

此组件将显示一个标志、一个标题以及指向我们用户资料的链接。如果我们使用 npm run dev 命令运行我们的应用程序,我们将看到以下内容:

图 4.1:显示新标题的伴侣应用登录页面

图 4.1:显示新标题的伴侣应用登录页面

我们组件的代码目前是纯 HTML,带有硬编码的数据,而我们需要更新它以使其动态化。

如果您曾经使用过互联网,您可能知道像前一个屏幕截图显示的这样的用户名不应该硬编码,而应该是动态的。

因此,我们的第一步是从 HTML 中删除这个硬编码的值,并将其转换为与我们在上一章中学到的 Ref 实例。

首先,我们将在脚本标签中创建一个带有用户名的 Ref 变量:

<script setup>
import { ref } from 'vue';
const username = ref("Zelig880")
</script>

然后,我们将使用我们的第一个内置指令将此变量分配给 HTML 元素,如下面的代码片段所示:

<header>
  <img src="img/50" />
  <h1>Companion app</h1>
  <a href="#">
    Welcome
    <span v-text="username"></span>
  </a>
</header>

使用内置的 v-text 指令将自动更改元素的 innerText,其值为我们的 JavaScript 变量 username

当然,在我们的例子中,名称仍然是硬编码的,因为我们的脚本中预定义了引用,但稍后这将改为从 API 动态获取。

如果您还记得我们之前的章节,我们已经成功地将动态文本分配给替换 SocialPost.vue 文件中的帖子、名称和用户名。我们使用类似于显示帖子的方式,通过 mustache 插值语法 {{ }} 实现了这一点:

<div class="post">{{ post }}</div>

因此,在这个阶段,你可能想知道这两种方法之间实际的区别是什么?

答案不会令人兴奋——事实上,这两种方法在渲染时都会编译成相同的代码。这两种方法,即使达到相同的目标,语法也不同,一个定义为使用v-text的属性,另一个直接作为元素内容的一部分使用{{ }}编写,并且会吸引不同风格的开发者。

另一个小差异是,v-text将替换innerHTML的全部内容,而使用 mustache 插值将允许你只替换部分文本。让我们通过一个示例来展示这些差异:

// v-text
<a href="#">
  Welcome
  <span v-text="username"></span>
</a>
// mustache interpolation
<a href="#">
  Welcome
  {{ username }}
</a>

在第一个使用v-text的例子中,我们必须添加一个<span>元素才能使用指令。实际上,如果我们直接将v-text指令分配给<a>标签,"欢迎"文本就会被覆盖。

在第二个使用双大括号的例子中,我们可以看到我们能够只更改字符串的一部分,而不需要额外的标记,这使得它更加灵活。

总之,使用v-text或 mustache 插值可以互换,没有对错之分,这完全取决于个人喜好。

使用v-html修改元素的内部 HTML

在前面的子节中,我们使用了我们的第一个 Vue.js 指令v-text。我们了解到这个指令替换了给定组件的innerText,但如果我们想用包含 HTML 的动态字符串替换文本怎么办?这就是v-html派上用场的地方。让我们深入了解如何使用它,并将其应用于我们的应用程序。

渲染 HTML 可能很危险

动态渲染 HTML 需要谨慎处理,因为它可能非常危险,并可能导致 XSS 漏洞。仅用于可信内容,绝不要用于用户提供的内容。

在解释v-html如何使用之前,我们首先应该尝试理解为什么你会在代码中使用这种技术。实际上,为什么你会在变量中添加 HTML,而不是直接在你的组件的<template>部分编写它呢?

我在职业生涯的开始时就问过自己这个问题,但后来我意识到有几个情况下v-html可能很有用,包括以下情况:

  • 当开发一个从 CMS 接收内容(如博客文章)的应用程序时,v-html可能非常有用。在某些情况下,这些内容是以 HTML 的形式接收的。

  • 第三方插件输出:有一些第三方插件,如代码可视化和画布可视化插件,可能需要使用 HTML。

  • 硬编码的 SVG 或图标:对于小型项目来说,创建一个包含图标和 SVG 等硬编码资源的库是非常常见的。

如您所见,v-html的实际用途仅限于一些非常具体的场景,并且不建议在所有字符串插值使用中推荐。

我们的头组件TheHeader.vue目前包含一个硬编码的 SVG 来显示我们的标志。我们可以使用v-html使图像动态化:

  1. 首先,创建一个名为 logo 的变量来保存我们的 SVG 代码:
<script setup>
import { ref } from 'vue';
const username = ref("Zelig880");
const logo = `<svg
    height="50"
    width="50"
    viewBox="0 0 210 210"
  >
  <polygon
    points="100,10 40,198 190,78 10,78 160,198"
    style="fill:grey;"/>
  </svg>`;
</script>
  1. 在我们的模板中应用 v-html 指令:
<template>
  <header>
    <div v-html="logo" ></div>
    <h1>Companion app</h1>
    <a href="#">
      Welcome
      <span v-text="username"></span>
    </a>
  </header>
</template>

完成了!现在我们的应用程序将显示一个星形作为标志,而不是我们之前的图片,如下面的截图所示。

图 4.2:伴侣应用头部

图 4.2:伴侣应用头部

这是一个简单的例子,并没有真正充分利用 v-html 的动态使用。实际上,在这种情况下,直接在 HTML 中放置 SVG 可能更合适,因为不需要为静态内容创建动态属性。

是时候继续前进,介绍如何切换组件的可见性了。在下一节中,我们将了解所有关于 v-showv-if 的内容,并开始看到使用如 Vue.js 这样的框架如何真正简化我们的代码。

使用 v-if 和 v-show 处理元素可见性

本节全部关于元素可见性。我们将学习两个实际上可以实现相同结果——切换元素可见性——但有两个潜在差异的指令。

切换元素的可见性意味着在渲染的 HTML 中显示或隐藏元素的能力。这是当今网络中一个非常常见的功能。这可能是简单的下拉菜单出现,当按钮被点击时显示模态框,或者当点击“显示更多”链接时显示更多数据。

切换可见性是许多支持前端框架被广泛采用以实现无瑕和响应式用户体验的许多功能中的第一个。首先,我们将在 SocialPost.vue 文件中添加一个按钮,该按钮仅在文章有评论时显示,然后我们将显示一个名为 SocialPostsComments.vue 的新组件,该组件将由新按钮切换,并在后续章节中用于显示帖子列表。

使用 v-show 从 DOM 中隐藏元素

能够显示或隐藏元素对于极其复杂的情况非常有用,但对于我们即将要讨论的非常简单的情况也同样有用。

如果你查看 TheWelcome.vue 中的帖子结构,你会注意到 posts 对象现在包括一些尚未显示的额外信息,例如评论和标签。

我们的第一步是在 SocialPost.vue 中显示一个按钮,用户可以点击以显示或隐藏帖子评论。由于并非所有帖子都有评论,我们只想为包含评论的文章显示此按钮。为了实现这一点,我们将使用 v-show

与所有其他指令一样,v-show 只是一个接受值的元素属性。在这种情况下,接受的值是 truefalse。当 v-show 接收到布尔值时,如果其值为真(如 <button v-show="true" />),则将显示元素,如果值为假(如 <button v-show="false" />),则将隐藏元素:

<template>
<div
  …
  <div class="post">{{ post }}</div>
  <button v-show="comments.length > 0">
    Show Comments
  </button>
</div>
</template>

在前面的代码中,我们已经访问了我们的SocialPost.vue文件并添加了一个新的按钮元素。然后,因为我们只想为有评论的帖子显示这个新元素,所以我们使用了我们新引入的v-show指令,并使用评论属性来定义何时切换元素的可见性。

因此,在我们的情况下,如果我们的帖子没有评论,comments.length > 0的值将等于false,当v-show接收到一个false值时,它将隐藏元素。另一方面,如果帖子有评论,comments.length > 0的值将是真值,按钮将被显示。前面的代码将显示以下结果:

图 4.3:伴随应用程序显示前面代码片段的结果,其中一条帖子显示了“显示评论”按钮

图 4.3:伴随应用程序显示前面代码片段的结果,其中一条帖子显示了“显示评论”按钮

请注意,当使用v-show时,我们实际上并没有从 DOM 中移除项目,只是隐藏了它。实际上,如果我们使用浏览器调试器分析 DOM,我们可以看到按钮在第二个帖子中是可用的,但只是被隐藏了。

图 4.4:Chrome DevTools 元素标签显示 DOM 中的按钮及其样式定义为“display:none”

图 4.4:Chrome DevTools 元素标签显示 DOM 中的按钮及其样式定义为“display:none”

虽然现在这似乎不太重要,但在下一节中,我们将学习v-if之后,它将更有意义。

使用 v-if 保持 DOM 清洁和性能

在前面的子节中,我们添加了一个按钮来显示评论,但这些评论仍然没有在屏幕上显示。在本节中,我们将通过工作一个新的组件并将“显示评论”按钮增强为切换这些组件的可见性来将这些评论添加到我们的 UI 中。

v-if指令的工作方式与v-show相同。它接受一个参数,该参数将根据其是真值还是假值来显示或隐藏指定的元素。

在我们的情况下,我们将创建一个新的 Ref,名为showComments,这将用于切换元素的可见性。让我们一步一步地在我们的SocialPost.vue文件中看看如何实现这一点。

  1. 我们将在SFC单文件组件)的<script>块中定义我们的新 Ref:

    const showComments = ref(false);
    
  2. 在脚本标签的顶部导入新的组件:

    import SocialPostComments from './SocialPostComments.vue';
    
  3. 将该组件添加到我们的 HTML 中。我们这样做是通过传递一个包含评论的属性,如下所示:

    <SocialPostComments :comments="comments"/>
    
  4. 添加v-if指令以确保组件仅在 showComments 私有数据为 true 时显示:

    <SocialPostComments
    
      v-if="showComments"
    
      :comments="comments"/>
    
  5. 如果我们现在检查应用程序,我们会注意到新的组件还没有显示。这是因为 showComments 的值被设置为false,而且没有方法可以改变它。让我们通过允许我们的“显示评论”按钮改变我们的 Ref 的值来解决这个问题:

    <button
    
      v-show="comments.length > 0"
    
      @click="showComments = true"
    
    >Show Comments</button>
    

如前述代码所示,我们使用了原生的 click 事件,它触发了一些基本的 JavaScript 代码来改变我们 Ref 的值。多亏了 Vue 的响应性,当 showComments 的值改变时,我们的 UI 将会自动重新渲染。如果你想要能够切换组件的开启和关闭,你可以写 showComment = !showComment 来确保 showComment 的值将等于当前 showComment 值的相反。

没有样式的组件应该看起来像这样:

<template>
  <div
  class="SocialPost"
  :class="{ SocialPost__selected: selected}"
  @click="selected = !selected"
>
  <div class="header">
    <img class="avatar" :src="img/avatarSrc" />
    <div class="name">{{ username }}</div>
    <div class="userId">{{ userId }}</div>
  </div>
  <div class="post">{{ post }}</div>
  <button
    v-show="comments.length > 0"
    @click="showComments = !showComments"
  >Show Comments</button>
  <SocialPostComments
    v-if="showComments"
    :comments="comments"
  />
</div>
</template>
<script setup >
import { onMounted, ref } from 'vue';
import SocialPostComments from './SocialPostComments.vue';
const showComments = ref(false);
const props = defineProps({
  username: String,
  userId: Number,
  avatarSrc: String,
  post: String,
  comments: Array
});
onMounted( () => {
  console.log(props.username);
});
</script>

在我们进入下一节之前,我们将学习如何渲染项目列表,我们需要明确 v-ifv-show 之间的区别。实际上,直到现在我们还没有定义究竟是什么让它们不同,似乎也没有理由我们不能在之前的例子中使用 v-show

在上一节中,我们强调了使用 v-show 时,HTML 元素会被渲染到 DOM 中但保持隐藏。然而,如果我们观察 v-if 的相同方面,我们会注意到元素完全缺失,并被一个 <!--v-if--> 的 HTML 注释所替代:

图 4.5:SocialPosts.vue 组件的 DOM 提取

图 4.5:SocialPosts.vue 组件的 DOM 提取

对于新手来说,这个差异可能看起来很小,但实际上有三个主要原因非常重要:

  • 如果没有从 HTML 中省略元素的能力,当我们尝试渲染一个需要特定值的组件(即使它是隐藏的)时,我们将会遇到错误。例如,在我们的案例中,如果我们使用了 v-show,Vue 编译器会尝试渲染组件,但这样做会失败,因为在某些情况下,评论变量可能不可用。

  • 在 DOM 中渲染大量未使用的元素确实会影响你的性能。比如说,如果我们每篇帖子有 150 条评论,并且使用了 v-show,那么 DOM 就会有成百上千的隐藏节点,这些节点可能永远不会被使用。使用 v-if 允许我们确保这些节点只有在需要时才被渲染。

是时候前进到下一节了,我们将学习一个新的指令,称为 v-for。这个指令允许我们通过自动复制我们的 HTML 来渲染项目列表,例如我们的评论。

使用 v-for 简化模板

本节中介绍的指令称为 v-for,它将被用来渲染元素列表。如果你学习过任何编程语言,你可能已经接触到了编程语言中用于逐条遍历列表的 for 循环的概念,而 v-for 指令与此类似。

使用 v-for 允许我们通过多次重新渲染相同的元素(或元素集)来渲染特定的列表。使用 v-for 不仅简化了我们的 HTML,还允许我们渲染那些在事先不知道确切条目数量时无法渲染的动态列表。

让我们看看原生 HTML 实现和 Vue.js 实现的简单列表是如何比较的:

图 4.6:原生 HTML 和 Vue.js 列表实现的比较

图 4.6:原生 HTML 和 Vue.js 列表实现的比较

如所示,v-for 指令的实现与其他您可能之前见过的 for 循环非常相似。该指令接受一个参数,格式为“item in array”,就像在主要编程语言中一样。

即使前面的例子看起来并不那么令人印象深刻,但随着 v-for 成为首选指令,并且将帮助您用优雅的代码实现复杂要求,这一切都将随着时间的推移而改变。

让我们继续开发 SocialPostComments.vue 文件,并利用 v-for 指令来显示特定帖子的不同评论。

我们使用 v-for 指令所需的步骤如下:

  1. 获取一个对象或数组类型的变量或属性。在我们的例子中,它作为一个属性可用。

  2. 创建第一个元素的 HTML。这有助于加快 HTML 开发速度。

  3. 将列表更改为使用 v-for 并自动创建重复条目。

第一步已经在 SocialPostComments.vue 文件中设置好了,作为使用 defineProps() 定义的数组属性:

const props = defineProps({
  comments: Array
})

接下来,我们将创建我们想要显示的 HTML。如前所述,我们将通过使用数组语法来硬编码数组的第一个条目:

<div>
  {{ comments[0] }}
</div>

我们硬编码第一个注释的上一段代码可能看起来不相关,但在更复杂的发展中,以这种方式创建 v-for 将为您节省大量时间。我使用这种仅渲染 v-for 列表中的一个条目的过程来专注于 HTML 的设计和开发。当列表元素完全设计完成后,我然后进行下一步。

最后,是时候实现我们的 v-for 指令了:

<div v-for="comment in comments">
  {{ comment }}
</div>

上一段代码将根据我们数组的长度渲染我们的 <div> 元素多次。

你知道吗?

v-for 指令的主体也可以用来加载其他 Vue 组件。这允许您将复杂代码抽象为其自己的组件,使您的应用程序更简洁。

在我们进入下一章之前,我们应该指出 v-for 可以接受多个元素,而不仅仅是之前显示的单个元素。为了在现实生活中展示这一点,我们将通过使用 index 数组为每个评论添加一个标题:

<div v-for="(comment, index) in comments">
  <strong>This is comment number {{index}}</strong>
  <p>{{ comments }}</p>
</div>

上一段代码展示了两个额外的添加。

首先,我们通过使用 (comment, index) 语法暴露了数组的索引。然后我们通过将简单字符串替换为多元素结构来改进 v-for 循环中的 HTML。

摘要

本章向您介绍了第一个 Vue 指令,并开始为您的 HTML 添加一些魔法。

总的来说,你学习了如何完成三个不同的用例。首先,我们向你展示了如何使用 v-textv-html 应用 HTML 和文本。这对于将动态变量绑定到组件模板中非常有用。然后,我们发现如何使用 v-ifv-show 来处理元素的可见性。我们发现,尽管它们在视觉上都能达到相同的结果,但实际上它们在幕后是不同的。v-show 使用 CSS 隐藏元素,而 v-if 则将元素从 DOM 中完全移除。最后,我们通过查看 v-for 以及它如何用于在列表中迭代并简化我们的 HTML 来结束本章。

你的回合

现在轮到你了,来做一些本节的练习。有多个地方可以练习指令,但这里有一些具体的建议。

  1. SocialPost.vue 重新整理,使用 v-show 来显示帖子的标签。(你需要为此添加一个新的属性。)

  2. TheWelcome.vue 重新整理,使用 v-for 来加载我们的帖子,而不是使用硬编码的实现方式。

  3. 回到你的页脚实现,并使用 v-textv-for 替换硬编码的链接。

在下一章中,我们将回到组件的逻辑部分,通过引入 计算属性 来增强我们的 Refs 和 props。本章的第二部分将介绍 方法,这些方法将包含我们的组件逻辑,并有助于进一步清理组件的 HTML。

第五章:利用 Vue.js 中的计算属性和方法

在上一章中,我们学习了组件的<template>部分。在本章中,我们将把重点转移到<script>标签上,并学习如何确保组件的逻辑被抽象化。

本章旨在向您介绍两个功能:方法计算属性。这些 Vue.js 功能对于确保组件逻辑保持高效和可读性至关重要。我们将回顾之前章节中创建的组件,并使用前面的技术使它们更容易阅读。在整个章节中,我们还将继续添加关于之前章节中学习到的功能的更多细节,例如 Refs 变量和指令。

本章将分为以下几部分:

  • 使用方法抽象功能

  • 使用计算属性创建动态数据

  • 学习方法和计算属性之间的区别

到本章结束时,你将能够将逻辑抽象到方法中,使用计算属性创建响应式变量,最重要的是,理解它们之间的区别。

技术要求

为了能够跟随本章内容,你应该使用名为CH05的分支。要拉取这个分支,请运行以下命令或使用您选择的 GUI 软件来支持此操作:

git switch CH05

本章的代码文件可以在github.com/PacktPublishing/Vue.js-3-for-Beginners找到。

使用方法抽象功能

在教程和培训中展示的大多数组件,例如本书中迄今为止分享的组件,看起来总是易于阅读且非常小巧,但这并不总是如此。在实际应用中编写的组件很容易因为逻辑而变得臃肿,难以阅读。大多数情况下,这种复杂性是多次迭代和功能变更的结果。

尽量保持组件尽可能干净是非常重要的。实现这一点的最简单方法是将逻辑从 HTML 中抽象出来,并将其移动到<script>标签内,但我们可以使用什么来简化我们的组件呢?

这就是方法发挥作用的地方。方法是可以用于 Vue.js 组件实例中的 JavaScript 函数。方法可以被描述为助手,允许你在组件内执行操作。

方法可以在各种情况下使用,从从 API 获取数据到验证用户输入,并且将成为我们简化组件逻辑的 Vue.js 首选功能。

在使用 Composition API 语法编写方法时非常简单,因为方法只是简单的 JavaScript 函数。

是时候检查我们在技术要求部分中提到的正确分支了,并看看方法在实际中的应用。

我们将查看components/molecules文件夹中的SocialPost.vue组件的 HTML,并尝试找到一些可以提取的逻辑。我们要找的是任何我们可以将之转换为函数的、写在我们组件 HTML 元素中的 JavaScript 代码:

<template>
<div
  class="SocialPost"
  :class="{ SocialPost__selected: selected}"
  @click="selected = !selected"
>
  <div class="header">
    <img class="avatar" :src="img/avatarSrc" />
    <div class="name">{{ username }}</div>
    <div class="userId">{{ userId }}</div>
  </div>
  <div class="post" v-text="post"></div>
  <button
    v-show="comments.length > 0"
    @click="showComments = !showComments"
  >
    Show Comments
  </button>
  <SocialPostComments
    v-if="showComments"
    :comments="comments"
  />
</div>
</template>

当你分析一个组件时,以下是最适合重构为方法的候选者:

  • 与事件(点击、更改)相关的逻辑

  • 需要参数(循环索引)的逻辑

  • 执行副作用逻辑

在我们前面的代码中,有两个与某些逻辑相关联的点击事件实例:第一个<DIV>中的@click="selected = !selected"Show Comments按钮中的@click="showComments = !showComments"

这些是很好的重构候选者,因为这种逻辑不应该存在于 DOM(文档对象模型)中,而应该移动到组件的<script>标签外部。重构此代码分为两个简单的步骤。首先,我们在<script>中创建一个方法,然后我们将逻辑替换为我们新创建的方法。

让我们在组件逻辑中创建两个函数,分别命名为onSelectedClickonShowCommentClick

让我们更新我们的代码:

<script setup >
import { onMounted, ref } from 'vue';
import SocialPostComments from './SocialPostComments.vue';
const selected = ref(false);
const onSelectedClick = () => {
  selected.value = !selected.value;
}
const showComments = ref(false);
const onShowCommentClick = () => {
  showComments.value = !showComments.value;
}
...
</script>

在事件处理程序前缀加上“on”

你可能已经注意到,我已将事件方法名称前缀为“on”,后缀为事件名称(click)。这是一个好的实践,因为它使代码更易于阅读,并帮助你识别与事件相关的方法。

多亏了 Composition API 语法,我们可以通过在 Ref 的初始化下方添加方法来按组组织我们的功能,使我们的代码更加整洁且易于阅读。

<script>标签中读取和写入 Ref

你可能已经注意到,代码与我们在 HTML 中使用的逻辑有所不同。selectedshowCommentsRef 的语法不同,因为它们后面跟着.value

在读取和写入<script>标签中的 Ref 时,向 Ref 添加.value是一个要求,这并不适用于可以通过普通变量访问的 Reactive 变量。

让我们通过一个视觉图来帮助阐明 Ref 和 Reactive 之间的差异:

图 5.1:显示在和标签中读取和写入 Ref 和 Reactive 时差异的表格

图 5.1:显示在<script><template>标签中读取和写入 Ref 和 Reactive 时差异的表格

我知道一开始这可能会让人感到困惑,但到书的结尾,你将掌握这个差异,因为我们在开发过程中会多次迭代并使用这个差异。

为什么 Ref 需要.value

Vue 的响应性是基于一个代理对象构建的,该对象监听变量的“get”和“set”事件。这个代理在原始值(如字符串、数字和布尔值,即 ref 使用的类型)中不可用。为了克服这一点,这些原始值被转换成具有单个属性 .value 的对象。

现在我们已经创建了方法,是时候调用它们了。我们将从组件的 HTML 中调用这些方法。为此,我们将移除之前分配给 @click 事件的逻辑,并用我们刚刚创建的方法替换它。

我们的主要 <div> 将看起来像这样:

<div
  class="SocialPost"
  :class="{ SocialPost__selected: selected}"
  @click="onSelectedClick"
>

我们的“显示评论”按钮将变成这样:

<button
  v-show="comments.length > 0"
  @click="onShowCommentClick"
>
  Show Comments
</button>

如前述代码所示,与我们的点击事件相关的逻辑已经被移动到了 <script> 标签中。

我们所做的改变可能看起来非常无关紧要,不值得这么做,但像这样的小可读性改进有助于使代码不仅易于阅读,而且易于更改。

让我们再次修改我们的组件,并假设我们被要求在组件的 <script> 标签每次触发时使用 console.log,它们可以很容易地扩展:

const showComments = ref(false);
const onShowCommentClick = () => {
  console.log("Showing comments");
  showComments.value = !showComments.value;
}

添加 console.log 简单得不能再简单了。我们使用现有的 JavaScript 知识修改了方法,如前述代码所示。

轮到你了

尝试创建你自己的方法。我的建议是尝试在组件挂载时向消息中添加 console.log(你可以使用 onMounted)或者继续通过创建一个记录应用中帖子数和评论数的方法来练习使用 Ref 和 Reactive。

你现在应该能够通过使用方法来重构你的组件,并提高代码的可读性。将复杂代码分解成更小的函数将帮助我们保持组件的可维护性。在下一节中,我们将查看 Vue.js 提供的类似功能,称为计算属性。

使用计算属性创建动态数据

在上一节中,我们学习了如何通过抽象点击事件的逻辑来简化我们的组件。我们将继续探讨“重构”的主题,并介绍一个名为计算属性的新特性。

当人们第一次了解计算属性时,他们通常会由于它们的相似性而将它们与方法进行比较,但事实上,这个特性与 props 和 Ref 的关系更密切。

计算属性可以这样描述:

计算属性使你能够创建一个动态属性,可以用来修改、操作和显示你的组件数据(refs、reactive 和属性)。- blog.logrocket.com/

到目前为止,我们已经使用了 Ref、Reactive 和 props 在我们的应用程序中传递和显示数据,但有时接收到的数据可能需要格式化,或者我们需要完全创建新的数据。

当需要动态属性时,计算属性是正确的特性来使用。

在我们开始修改配套应用程序之前,让我们介绍一些现实生活中的例子,以帮助更好地理解计算属性及其目标。

  • 场景 1 – 学生名单

    假设我们从 API 接收数据,该数据返回学校中的所有学生,但我们只想过滤出女学生。我们可以使用计算属性来创建一个过滤后的数组。

  • 场景 2 – 切换数组长度

    在这种情况下,我们被要求只在我们列表中有五个或更多条目时显示按钮。我们可以使用计算属性来检查条目的长度,并创建一个具有布尔值的新的属性。

  • 场景 3 – 连接值

    想象一个应用程序,它有名字和姓氏作为属性,而你希望访问一个包含全名的变量。嗯,我们可以使用计算属性来创建这个值,它将取决于名字和姓氏。

  • 场景 4 – 片段

    你是否遇到过这样的博客,你只能看到博客文章的一小部分,然后可以点击“显示更多”来显示完整文章?嗯,为了实现这一点,我们可以创建计算属性,它只返回一定数量的字符。

如果你重新阅读前面的例子,你可能注意到它们都有共同之处。事实上,我们提出的所有场景都有以下相似之处:

  • 它们都创建了一个在组件内部需要的新数据/值

  • 它们不会产生任何副作用

  • 它们都依赖于另一份数据(props、Refs 或 Reactive)

前面的要点出现在我提出的所有场景中,这不是因为我选择了它们,而是因为它们是计算属性的前提条件。

计算属性是一种特性,它允许我们使用一个或多个现有的数据片段(Refs、Reactive 和 props)来创建一个新的值。

是时候开始看看一些例子,以便了解如何以及何时使用这个特性了。

计算属性的语法如下:

import { computed } from 'vue'
const test = computed( ... );

使用计算属性时,我们首先需要从 Vue 中导入它,然后将其分配给一个新的常量。计算属性访问一个回调,该回调将包含创建新变量所需的逻辑。

让我们看看一个简单的例子,并展示一个创建全名的计算属性将是什么样子:

<script setup>
import { ref, computed } from 'vue'
const name = ref("Simone");
const surname = ref("Cuomo");
const fullName = computed( () => {
  return `${name.value} ${surname.value}`;
} );
</script>

在前面的代码中,我们创建了一个名为 fullName 的新动态属性。这将像普通属性一样工作,可以在组件的任何地方使用,而无需我们的组件。

就像我们之前说的那样,计算属性满足了我们的要求,具体如下:它创建新的数据(fullName),依赖于另一个值(namesurname),并返回一个值。

将计算属性添加到配套应用程序中

现在是时候更新我们的组件,看看我们如何利用计算属性进一步简化我们的代码了。

就像在前一个部分一样,我们将继续修改SocialPost.vue文件。让我们回顾一下这个文件,并尝试看看什么是一个好的计算属性候选者:

<template>
  <div
    class="SocialPost"
    :class="{ SocialPost__selected: selected}"
    @click="onSelectedClick"
  >
    <div class="header">
    <img class="avatar" :src="img/avatarSrc" />
    <div class="name">{{ username }}</div>
    <div class="userId">{{ userId }}</div>
  </div>
  <div class="post" v-text="post"></div>
  <button
    v-show="comments.length > 0"
    @click="onShowCommentClick"
  >
    Show Comments
  </button>
  <SocialPostComments
    v-if="showComments"
    :comments="comments"
  />
</div>
</template>

从前面的代码中,我们可以看到与v-show指令相关的逻辑已经被突出显示。这是我们最好的候选者,可以将其转换为计算属性。

计算属性需要返回一个依赖于另一个值且没有副作用的值,前面突出显示的逻辑正是如此。实际上,它返回一个truefalse的布尔值,这取决于comments的值,并且没有其他可以定义为副作用的操作。

就像方法一样,为了能够将这个逻辑转换为计算属性,我们需要将其逻辑移动到组件的<script>标签中。让我们看看实现这一目标所需的步骤:

  1. 首先,我们需要在<script>标签的顶部导入computed

    import { onMounted, ref, computed } from 'vue';
    
  2. 然后,我们创建一个使用计算属性的函数:

    const hasComments = computed( );
    
  3. 接下来,我们将把我们的逻辑作为计算方法的第一个参数添加为一个回调:

    const hasComments = computed(() => {
    
      return props.comments.length > 0;
    
    });
    
  4. 最后,我们将用新的计算属性替换 HTML 中现有的逻辑:

    <button
    
      v-show="hasComments"
    
      @click="onShowCommentClick"
    
    >
    
      Show Comments
    
    </button>
    

通过前面的代码,我们已经在我们的组件中创建了一个名为hasComments的新属性。这个属性,就像每个其他的 Vue.js 变量一样,是响应式的,并且会在comments数组变化时立即改变。

使用缓存值提高性能

创建计算属性不仅提高了我们组件的可读性,还通过缓存值提高了我们应用程序的性能。

这意味着计算属性中包含的实际函数仅在组件挂载时运行一次,除非依赖的值发生变化,否则在组件渲染时不会再次运行。

这可能不会对我们的简单示例带来巨大的改进,但在大型应用程序中,计算属性实际上可能是一个包含 100 多个条目的大型数组,这确实会带来很大的差异!

格式化你的数据

计算属性在其他的框架中并不常见,对于大多数 Vue.js 开发者来说,它们被视为一种新的技术/特性。由于它们的独特性质,有时在现实生活中掌握和使用它们可能会有点困难。为了确保这个主题被完全理解,我们将创建一些额外的示例,这些示例将使用计算属性。

如果你打开名为TheWelcome.vue的文件并检查posts响应式值,你会注意到对象中有额外的参数。实际上,likesretweets的值已经添加到我们的帖子中。

图 5.2:包含额外点赞和转发条目的响应式属性

图 5.2:包含额外点赞和转发条目的响应式属性

在本节中,我们将创建一个新的动态属性,用于存储交互的总数。我们将通过添加所有不同的交互来实现这一点,包括评论、点赞和转发。

这个新的计算属性将被命名为 interactions,在我们的帖子有 2 个评论、2 个点赞和 1 个转发的情况下,它将返回一个值为 5(2 + 2 + 1)。

这个例子应该能帮助你理解计算属性不仅仅是一种美化组件的方式,实际上它是一个强大的工具,可以帮助你增强应用程序的功能。

试试看

你为什么不试试看,尝试自己创建计算属性?创建这个计算属性需要你将到目前为止所学的一切付诸实践,所以为什么不接受一点挑战呢?

成功创建一个用于显示我们交互的计算属性需要几个步骤。以下图表显示了数据如何通过组件流动,这将有助于我们理解定义计算属性所需采取的步骤。

图 5.3:从父组件到计算属性的数据流

图 5.3:从父组件到计算属性的数据流

在这本书中,我们已经涵盖了前面的每个条目,但这次将是第一次我们将它们全部在一个单独的练习中使用。在开发生活中生成这样的完整数据流将会非常常见,因此提前进行一些练习是有益的。

让我们一步一步地回顾前面的数据流:

  1. 在父组件中设置数据

    让我们打开 TheWelcome.vue 并检查

    
    为了更好地样式化我们的两个布局,我们声明了两个类。第一个是 `sidebar__closed`,用于减小侧边栏的宽度。第二个是 `sidebar__icon`,用于定义箭头的大小和位置。
    
    `sidebar__icon` 应用于两个图标,而 `sidebar__closed` 仅在 `closed` 的值为 `true` 时分配给 `<aside>`。为此,我们使用了 `:class="{ 'sidebar__closed': closed}"` 语法。这种语法很有用,因为它允许你在满足特定条件时轻松地应用类,从而创建复杂的样式。
    
    在这个阶段,侧边栏不仅功能正常,而且其折叠和展开布局也被正确地样式化了。剩下要做的就是使数据 *持久化*。在开发中,我们描述数据为持久化,当其值在浏览器刷新后仍然保持一致时。
    
    ![图 9.5:展开的侧边栏](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/vue3-bgn/img/B21130_09_05.jpg)
    
    图 9.5:展开的侧边栏
    
    ![图 9.6:折叠的侧边栏](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/vue3-bgn/img/B21130_09_06.jpg)
    
    图 9.6:折叠的侧边栏
    
    现在侧边栏的外观已经得到改善,是时候让它刷新后也能保持其价值了。
    
    ## 在 localStorage 中保存和读取用户偏好
    
    在这个阶段,即使侧边栏逻辑完全正常工作,其数据还没有持久化。实际上,如果你将侧边栏设置为折叠状态并刷新页面,你会看到它会回到默认的展开视图(我们用来初始化 `closed` 引用的值是 `false`)。
    
    为了实现持久性,我们将使用 `localStorage` 来保存我们的值,并在页面加载时重新读取它。
    
    在我们查看代码之前,我们应该尝试定义实现这一目标的最佳方法。实际上,本节是以 Vue.js 生命周期命名的,但到目前为止,我们还没有使用它们。
    
    为什么正确使用生命周期很重要?
    
    在继续前进之前,花几分钟时间尝试理解使用不正确的生命周期从 `localStorage` 加载数据的后果。考虑不同的生命周期,它们何时被触发,以及它们可能对应用程序产生的影响。
    
    如我们在 *第二章* 中所学,存在不同的生命周期,支持不同的用例。在我们的场景中,我们计划使用一个生命周期来读取我们的 `closed` 变量的值,并将其应用于组件。在执行此类操作时,你通常应该问自己几个问题。第一个问题是数据是否异步,第二个问题是数据是否在应用程序渲染到屏幕之前就必需。
    
    每个生命周期都在组件生命周期的不同阶段发生。例如,`beforeCreate` 在组件甚至创建之前就被触发,而其他如 `onMounted` 则在组件完全挂载到 DOM 中后触发,因此选择正确的生命周期对于我们的特定场景非常重要。
    
    在我们的情况下,数据是从 `localStorage` 中获取的,这是一个同步操作;它需要在组件完全渲染或显示之前,也称为“在挂载之前”。
    
    最适合我们需求的生命周期是`onBeforeMount`。这将触发在组件渲染之前,但在所有方法和 Refs 初始化之后。
    
    让我们将这个逻辑添加到我们的组件中:
    
    ```js
    <script setup>
    import { ref, onBeforeMount } from 'vue';
    import TheButton from '../atoms/TheButton.vue'
    import IconLeftArrow from '../icons/IconLeftArrow.vue'
    import IconRightArrow from '../icons/IconRightArrow.vue'
    const currentTime = ref(new Date().toLocaleTimeString());
    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";
    });
    </script>
    

    为了实现持久性,我们首先从 Vue 包中导入了onBeforeMount方法,然后每次触发toggleSidebar方法时,我们都保存closed Ref 的值。为了实现这一点,我们使用了window对象中可用的localStorage.setItem方法。最后,我们使用onBeforeMount从 localStorage 中读取值,并将其分配给closed ref。

    在这个阶段,我们的应用不仅允许用户切换侧边栏,而且其值在刷新时也会保持持久。

    在完成本章之前,我想重点强调为什么正确使用生命周期很重要。实际上,如果我们使用了另一个生命周期,比如onMounted,那么在读取并应用localStorage的值之前,侧边栏就已经被完全渲染(错误地)了。这类 bug 的主要问题是,在开发过程中它们可能不会复现,或者非常难以发现。

    在创建将要改变组件视图的代码时,确保你已经使用了正确的生活周期,如果处理异步数据,那么在组件的其他部分执行之前,必须定义正确的加载状态,或者等待 promise 完成。这种做法一开始可能难以理解,但代码实践和错误将帮助你提高对 Vue.js 组件的理解,并提高你的技能。

    摘要

    在本章中,我们介绍了一些高级主题,例如插槽、生命周期和 refs。本章的目标并不是提供你在这方面的所有信息,而是让你对这些概念有所了解,以便你在接下来的开发中能够实践它们,并在扩展你对 Vue.js 知识的过程中继续学习。

    我们已经学习了如何使用插槽来扩展组件的灵活性。插槽和命名插槽可以用于简单的情况,例如<button>、样式元素,如<div>,或者用于更高级的技术,例如定义具有不同区域的页面布局。

    然后,我们继续讨论模板Ref,这是一个我们在之前章节中部分介绍过的主题。我们学习了如何使用模板Ref来访问组件的 DOM 元素。这被定义为一种高级技术,因为,在 Vue.js 提供的所有功能中,你很少需要以这种方式使用模板Ref

    最后,我们再次回顾了生命周期。Vue.js 的生命周期非常重要,需要大量的实践来帮助你理解它们的用法以及,更重要的是,它们的执行顺序。我们在我们的伴侣应用中增加了一个额外功能,以便我们能够理解其一个使用案例,并思考如果使用不同的生命周期会产生什么样的可能结果。

    在下一章中,我们将学习如何使用vue-router定义多个路由。对于大多数应用来说,定义多个页面是一个必要的步骤,而vue-router提供了一个非常简单的语法,这将帮助我们在我们的小伴侣应用中实现这一功能。

    第十章:使用 Vue Router 处理路由

    单页应用程序SPAs),例如 Vue.js 提供的,基于单页面的架构。这种方法防止页面完全重新加载,并提供了改进的用户体验。

    随着您的应用程序增长,您将需要在应用程序中创建不同的视图。即使术语 SPA 可能会导致您认为您的应用程序将建立在单个页面上,但事实远非如此。

    大多数框架,包括 Vue.js,都提供旨在在其他框架(如 PHP、.NET 等)中重现路由系统的包。SPA 框架提供的路由功能提供了两者的最佳结合。它为开发者提供了一个完整的路由工具包,同时仍然提供了 SPA 框架预期的相同用户体验。Vue.js 中使用的路由包称为 vue-router。

    在本章中,您将学习如何在您的 Vue.js 应用程序中使用路由。在本章结束时,您将很好地理解 vue-router 及其配置,以及路由是如何定义和使用的。您将能够创建基本、动态和嵌套路由,并使用不同的方法导航到它们。最后,您将学习如何使用 redirect 和别名来提高用户体验。

    我们首先将通过介绍其配置来了解 vue-router。然后,我们将通过创建几个静态页面来学习如何实现我们的第一个路由和路由导航。接下来,我们将通过定义用户个人资料页面来介绍动态路由。然后,我们将通过使用嵌套路由将用户视图拆分为用户个人资料和用户帖子来添加另一个导航级别。最后,我们将通过熟悉 redirectalias 来完成本章。

    本章将分为以下部分:

    • 介绍 vue-router

    • 在路由间导航

    • 动态路由匹配

    • 嵌套路由

    • 使用 aliasredirect 重复使用路由

    技术要求

    在本章中,该分支被称为 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 通常由createVueVite等工具预设,了解它在幕后是如何设置的仍然很重要。

    插件工作所需的配置存储在一个通常命名为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 配置的对象。

    接下来,是时候看看配置对象中的第一个条目,historyhistory属性定义了您的应用程序如何在不同的页面之间导航。在标准应用程序中,这通常是通过将网站 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 定义了我们的应用程序将用户引导到特定页面的能力。因此,当声明路由时,我们定义了用户可以在我们的网站上访问的页面。

    要声明一个路由,我们需要两个信息,pathcomponent,但我通常更喜欢总是包括第三个称为 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的文件夹中。如果我们访问这个文件夹,我们会看到我们目前有两个视图设置,HomeAbout

    图 10.1:显示视图文件夹内容的文件夹树

    图 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>
    

    你可能已经注意到,文件内容仅定义了两个槽位(headingdefault)并且缺少页脚。这是故意为之,因为页脚有一个我们想要显示的默认值。

    在我们的新组件可以被访问和显示之前,我们需要将其添加到我们的路由中。让我们通过回到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,然后我们定义了一个名为privacyname值,用于未来的程序化导航,最后,我们将导入的PrivacyView组件分配给它。

    在这些更改之后,我们应该可以通过访问http://localhost:5173/privacy来看到我们的页面:

    图 10.2:隐私页面的截图

    图 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:带有路由链接的伴随应用侧边栏

    图 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:用户全信息的模式信息

    图 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的值已被设置为简单的数组,并且没有使用refreactive。这是故意为之,因为这个数组预计不会被修改,因此不需要是响应式的。在你开始 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/1234website.com/user/my-user-id

    正如我们之前提到的,替换参数的值将在 Vue 组件内部可用,并可用于渲染唯一的页面。在我们的例子中,提供的 ID 将用于加载特定用户的信息,因此访问不同 ID 的 URL 将渲染不同的用户信息页面。

    你可以在一个路由中拥有多个参数

    你知道你可以在单个路由中拥有多个动态参数吗?每个参数都将使用相同的语法定义。例如,你可以定义一个 URL 为/account/:accountId/user/:userId。将加载此路由的组件将能够访问两个动态参数,一个包含账户 ID,另一个包含用户 ID。

    使用路由名称添加导航

    现在用户路由已经定义,是时候对我们的设计做一些更改,以确保用户可以导航到它。

    即使我们可以通过使用之前章节中学到的导航轻松完成这个任务,我们还是要抓住这个机会来介绍一种新的语法,即使用路由名称进行导航。

    在定义新路由时,我们为每个路由项提供了pathname值。使用路径导航对基本 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

    使用UseRouterUseRoute

    你可能已经注意到,在之前的组件中,我们使用了useRouter,而在当前组件中,我们使用了useRoute。这两个名字非常相似,它们之间的差异很容易被忽略。useRouter用于我们需要访问路由对象时——例如,添加路由、推送导航和触发路由的前后操作——而useRoute用于获取当前路由的信息,如参数、路径或 URL 查询信息。

    在这个阶段,动态路由将完全可用,点击用户头像将加载该特定用户的信息。

    图 10.5:显示用户信息的用户视图页面

    图 10.5:显示用户信息的用户视图页面

    在本节中,我们学习了动态路由的所有不同方面。我们了解了为什么需要它们以及它们试图解决的问题,并且通过在我们的route数组中添加一个动态路由来介绍它们的语法。在此过程中,我们利用机会学习了如何使用路由的nameparams值进行程序化导航,并最终学习了如何使用 vue-router 包提供的路由对象来访问路由信息,如params,从而使我们的用户视图动态化。

    在完成本节的同时,我们还回顾了之前学习过的主题,如组件、指令、事件等等。

    在下一节中,我们将进一步学习嵌套路由。这将遵循与本节类似的流程,并希望对你来说感觉熟悉。

    学习嵌套路由

    单页应用(SPAs)非常强大,但添加一个结构良好的路由器,如 vue-router,可以帮助将 SPAs 提升到另一个层次。SPA 的强大之处在于其能够在不刷新页面的情况下动态交换组件的能力,但如果你告诉我 vue-router 甚至可以比页面布局更深层次,你会怎么想?这就是嵌套路由发挥作用的地方。

    嵌套路由允许你在路由中定义路由,可以有多层深度,以创建一个非常复杂的布局。嵌套路由的概念可能听起来很复杂,但它们的使用使得应用程序更容易开发,并且对于大多数应用程序来说都是推荐的。

    当我们创建主路由时,我们说当进行导航时页面将完全交换;对于嵌套路由,概念相同,但不是交换整个页面,而是只交换页面的一部分内部内容。

    图 10.6:嵌套路由示例

    图 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 属性来标识。这个属性接受一个数组形式的路由,其结构与我们之前使用的 namepathcomponent 相同。在这里你需要考虑的唯一一点是,当我们定义嵌套路由时,我们不再处于项目的根目录,因此分配给 path 的值会被添加到现有的路径上。所以,在这种情况下,userProfile 路由的完整路径将是 mysite.com/user/:userId/profile

    用户资料和用户帖子视图已经准备好通过导航到 http://localhost:5173/user/:userId/profilehttp://localhost:5173/user/:userId/posts(其中 userId 被替换为实际的用户 ID)来访问。

    嵌套路由并不意味着嵌套布局

    你可能已经注意到,在 图 10.6 的例子中,我们提到嵌套路由可以共享布局,并用于更新页面的一部分,例如内部标签页,但在这个例子中,情况并非如此,因为我们使用嵌套路由来完全更新页面。我们所做的是不常见的。事实上,嵌套路由的使用不仅仅是为了共享布局,也是为了共享特定的数据。在我们的例子中,嵌套路由的使用是为了允许两个路由共享名为 userIdpath 参数。如果你想进一步练习,你可以创建一个用于用户路径的组件,并将其用作用户资料和帖子路由的布局。只需记住,在你的 HTML 中添加一个 <RouterView> 组件来定义 vue-router 应该附加路由的位置。

    在本节中,我们学习了嵌套路由的概念。然后,我们将我们的伴侣应用更新,以使用嵌套路由来加载两个不同的用户视图。我们还创建了一个新的视图来显示用户帖子。接下来,我们学习了并应用了实现嵌套路由所需的语法。最终产品是我们能够导航到两个不同的页面,一个显示用户个人资料信息,另一个显示用户帖子。

    您可能已经注意到,我们已经将user路由移动到了其子路由之一。因此,如果我们访问user路由(例如,http://localhost:5173/user/60d21b4667d0d8992e610c85),我们将看到一个空页面。这不是一个错误,而是预期的,因为我们实际上没有声明任何满足该特定端点的路由。我们现在将通过创建别名来解决这个问题。

    使用别名和重定向重用路由

    到目前为止,我们已经学习了如何创建新路由,但掌握 vue-router 还需要另一项技能:别名重定向

    aliasredirect都允许您通过将用户从一个路由导航到另一个路由来重用现有路由,这在您想要创建 SEO 友好的 URL 时非常有用。这两个功能之间的唯一区别是用户在浏览器中看到的结果。

    使用我们之前的示例,我们发现自己处于一个路由目前无法使用的情况,因为它没有组件或视图与之关联。这是在处理children路由时常见的情况,可以通过别名或重定向轻松解决。

    为了解决我们的空路由问题,我们将使用redirect将所有到达user/:userId路径的用户导航到其子路径user/:userId/profile

    创建别名或重定向与简单路由相同,只是增加了一个redirectalias属性,用于指定应使用哪个视图。

    让我们更改我们的用户视图以包含redirect属性:

    path: "/user/:userId",
    name: "user",
    redirect : { name: "user-profile" },
    children: [
    

    通过简单地添加高亮代码,用户现在将被重定向到另一个路径。如果您尝试访问路径http://localhost:5173/user/60d21bf967d0d8992e610e9b,您将立即看到 URL 变为http://localhost:5173/user/60d21bf967d0d8992e610e9b/profile

    redirect的值可以是包含名称的对象,如我们的示例所示,一个简单的路径,甚至可以是一个包含nameparams等更多内容的复杂对象。

    现在是时候介绍alias了。之前,我们看到了我们可以使用redirect将用户从一个路由传输到另一个路由。但如果你只想确保用户能够通过多个 URL 访问给定的路由,那会怎么样?一个很好的例子是我们需要创建一个“友好 URL”,这是一个用户容易输入或记住的 URL。对于网站来说,如电子商务网站,在用户浏览网站时定义一个特定的 URL 是很常见的,同时提供一个友好 URL,这通常是 Google 索引和使用的 URL。

    仅举一个例子,如果你希望当用户访问mywebsite.com/p/123mywebsite.com/product/123时渲染相同的路由,我们可以使用别名来实现这一点。为了达到这个目的,我们可以使用别名。

    当使用别名时,我们可以定义一个或多个可以与同一路由匹配的 URL。别名接受一个字符串或字符串数组,这些字符串匹配给定路由的不同 URL。

    让我们假设,例如,我们希望我们的静态/privacy页面在用户访问/privacy-policy时也能渲染。为了实现这一点,我们将在路由中写下以下规则:

    {
      path: '/privacy',
      name: 'privacy',
      alias: '/privacy-policy',
      component: PrivacyView,
    },
    

    在我们的路由声明中添加别名后,我们的伴侣应用将为localhost:5173/privacylocalhost:5173/privacy-policy两个地址都渲染隐私页面。

    在这个小节中,我们介绍了aliasredirect的方法。我们定义了这两个方法解决的问题及其区别。最后,我们在自己的路由定义中实现了这个方法,以防止用户在访问user端点时看到空白页面,并提供了两个端点定义一个路由的简单示例来展示别名。

    摘要

    在本章中,我们学习了 vue-router,这是书中将要介绍的 Vue 核心库集的第一个外部包。本章通过介绍其配置和设置,如历史模式,向我们介绍了这个包。然后我们学习了路由的使用,如何定义它们,以及如何导航到它们。

    在学习完 vue-router 的基础知识后,我们继续深入到更高级的主题,例如动态路由和嵌套路由。

    最后,我们学习了aliasredirect,以完成我们对路由及其如何用于构建简单和复杂应用的基本理解。

    在下一章中,我们将学习 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:属性传递到多个组件层

    图 11.1:属性传递到多个组件层

    什么是属性钻取?

    从父组件传递数据到子组件的过程,尤其是多层深度的传递,也被称为属性钻取。当谈论状态管理时,这是一个常用的术语,也将在本章的其余部分中使用。

    让我们分析几个项目示例,看看我们是否需要 Pinia 的支持来处理状态管理:

    • 宣传册网站:一个包含几个静态页面的简单网站,非常适合本地企业。该网站可能包含一个联系表单。没有真正需要管理的数据。

      不需要存储

    • 个人博客:这个网站稍微复杂一些,具有动态页面,可以渲染我们在 Vue Router 章节中学到的博客页面。数据被传递到页面,但在应用程序的多个部分中不需要修改或使用。

      不需要存储

    • 电子商务网站:一个用于销售产品的商业网站。该网站将主要是动态的,提供大量的交互性。数据需要通过应用程序的多个层传递和修改,例如从购物车到结账。

      需要存储

    • 社交媒体网站:一个用于创建和分享帖子的网站。该网站还将有按用户、标签、类别等过滤的页面。相同的数据可以在应用程序的许多部分中重复使用,使用存储可以确保数据只被获取一次,然后重复使用。

      需要存储

    对于所有网站来说,添加状态管理器并不是必须的,但它是由您特定 SPA 的用例和需求驱动的需求。诚然,随着您职业生涯的进步,您将熟悉框架提供的工具和技术,您会发现您经常过度设计您的应用程序。然而,在您职业生涯的初期,保持简洁并只选择您真正需要的 SPA 工具是非常重要的。

    在进入下一节之前,我们应该对使用状态管理的真正好处说几句。我们将通过比较两个具有相同组件结构但处理数据方式不同的应用程序来实现这一点。一个使用属性钻取,而另一个使用状态管理。

    我们将从 图 11.1 中所示的示例开始。这个示例类似于我们的伴侣应用,它展示了 post 属性从 App.vue 组件流向屏幕上渲染的最后组件的方式。在这个阶段,应用程序仍然相当简单。即使有一些多层属性钻取发生,复杂性仍然是可接受的。

    现在,我们将添加用户编辑帖子的可能性。由于属性可以在定义它们的组件中简单地修改,因此我们不得不在整个组件树中发出一个“编辑”事件:

    图 11.2:属性向下传递,事件向上传递到组件树

    图 11.2:属性向下传递,事件向上传递到组件树

    这里的情况开始变得更加复杂。除了属性钻取之外,我们还有 Post 属性。

    我们将包括一个 Pinia 存储,看看它会给桌面带来什么变化。Pinia 将接管 post 属性,并将其提供给需要读取或修改它的组件:

    图 11.3:一个 Pinia 存储管理数据

    图 11.3:一个 Pinia 存储管理数据

    添加存储已经从我们的应用程序中移除了很多复杂性。当我们编辑帖子时,现在唯一需要重新渲染的组件将是 帖子正文。此外,存储使我们能够轻松地向下传递特定数据,例如侧边栏中的 最近帖子。这样做也将确保除非更改的帖子包含在侧边栏中,否则侧边栏不会重新渲染(这也可能使用属性实现,但通常不这样做)。

    在这个简短的章节中,我们定义了何时需要状态管理以及何时应该从您的应用程序中省略它。然后,我们通过描述一些示例应用程序来定义在您的应用程序中何时需要存储。最后,我们介绍了状态管理的概念,并涵盖了它为您的应用程序带来的好处。

    在下一节中,我们将学习 Pinia 的结构以及我们如何在应用程序中使用它。

    了解 Pinia 存储的结构

    在本章中,我们将介绍构成 Pinia 存储的内容以及它是如何支持您管理应用程序数据的。Pinia 建立在多个存储的概念之上。每个单独的存储都将管理一组特定的数据或公司逻辑,这些数据或逻辑不绑定到特定的组件。您的应用程序可以有一个帖子存储、评论存储,甚至一个用于管理侧边栏状态的存储。

    存储可以相互通信,但最重要的是您应该能够轻松定义单个存储的特点。存储的数据应该很好地分割。

    每个存储被分为三个不同的部分:stategettersactions

    这三组在 Pinia 存储中可用的选项实际上可以与我们关于 Vue 单文件组件所了解的现有功能进行比较。

    Pinia 中定义的 state 对象与用作私有组件数据的 Ref 或 Reactive 相当。getters 与计算属性相当,因为它们用于创建现有 state 的修改版本。最后,我们有 actions,它们类似于方法,用于在存储上执行副作用。

    图 11.4:Pinia 选项与 Vue 组件之间的比较

    图 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 中使用:获取器。

    获取器与计算属性类似。它们允许你使用存储状态来创建变量。在我们的例子中,我们将引入一个简单的获取器,为我们的侧边栏创建一个友好的openclosed标签。在除这个用例之外,获取器还可以用于翻译目的、过滤数组或规范化数据。

    让我们回到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:带有侧边栏状态的伴侣应用程序标题

    图 11.5:带有侧边栏状态的伴侣应用程序标题

    这是一个简单的例子,帮助我们学习如何重构现有代码,以及如何定义具有状态、getter 和 actions 的存储。最后,它帮助我们学习如何在应用程序中的一个或多个组件中使用存储。

    我们将通过在伴侣应用程序中引入另一个存储来继续我们的 Pinia 学习之旅。在下一节中,我们将创建一个将处理我们的帖子的存储。这将比之前的更复杂一些,并允许我们介绍状态管理器提供的更多功能。

    使用 Pinia 创建帖子存储

    在应用程序中添加状态管理是一个永无止境的任务,因为随着应用程序的增长,它也在不断进化。到目前为止我们在应用程序中所做的工作——通过将逻辑从组件中移出并放入存储中来进行应用程序的重构——是一种常见的做法。正如我们之前提到的,将侧边栏逻辑移入存储中有点过于激进,在真实的应用程序中并不期望这样做,因为逻辑足够小,足以在组件内部运行(即使有属性钻取)。

    在本节中,情况有所不同。我们将重构应用程序的一个关键部分到存储中:帖子。处理帖子的数据获取和管理是应用程序的一个关键部分,并且随着应用程序的增长可能会变得更加复杂。正因为如此,将帖子移入存储将改善应用程序的整体结构。

    就像之前一样,我们将通过首先分析代码库以找到所有与帖子相关的代码来重构当前代码。然后,我们将创建一个新的存储并将代码移到那里。最后,我们将更新组件以使用存储。

    由于这是我们第二次进行这个练习,我将跳过一些步骤,直接进入存储的创建。在你跳到下一节之前,我建议你搜索所有与帖子相关的所有方法,并将它们与我们将在存储中编写的那些方法进行比较。这个练习将非常有价值,因为它将为你提供关于你对应用程序当前理解的洞察。

    我们的新存储将被命名为posts.vue。它将保存在src/stores文件夹中,就像我们之前的存储一样。

    此存储将包含具有 postspage 属性的状态,以及两个不同的操作:fetchPostsremovePosts

    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 的操作。与前存在于组件中的方法以及我们添加到存储中的副本之间的主要区别是我们访问变量(如 pageposts)的方式。在 Pinia 存储中,可以通过 page.value 访问状态值,我们将它改为 this.page

    这是我们将要为方法做出的唯一更改,因为其余的逻辑保持不变,并且不需要任何修改。

    与上一节相比,我们还没有引入任何新内容,对帖子存储的重构流程与侧边栏存储实现非常相似。将逻辑重构到存储中通常是一个非常简单的练习,我们可以像在两个示例中那样提升和转移大部分逻辑。

    现在存储已设置,我们将把注意力转向组件,确保它使用存储状态和操作。在这个过程中,我们将学习如何解构 Pinia 存储以增加组件的可读性。

    直接使用如 const { test, test2 } = useTestStore() 这样的语法解构存储是不可能的,因为这会破坏此状态的响应性。

    打破响应性意味着如果存储中的值发生变化,该变化将不再传播到组件中。为了解决这个问题,Pinia 提供了一个 storeToRefs 方法,它将允许我们安全地解构 Pinia 存储。

    存储或 ref 是个人喜好

    使用存储,就像我们在之前的示例中所做的那样,或者将值解构为 Refs 是一个完全个人的选择。没有对错之分。

    直接使用存储将明确定义来自存储的数据,因为它将使用存储名称作为所有数据的前缀,例如 myStore.firstName。另一方面,使用 Refs 将生成一个更干净的组件,因为状态不需要前缀,访问状态只需使用 ref 的名称,例如 firstName

    让我们通过将组件更改为使用存储并使用 storeToRefs 方法来完成我们的存储迁移。由于文件包含许多更改,我们将将其分解为多个阶段:

    1. 初始化存储:

      import { usePostsStore } from '../../stores/posts';
      
      import { storeToRefs } from 'pinia'
      
      const postsStore = usePostsStore();
      
    2. private 状态替换为 store 状态:

      const { posts } = storeToRefs(postsStore);
      
    3. 将方法替换为存储操作:

      const { fetchPosts, removePost } = postsStore;
      
    4. 更新 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-modelevent/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>
    

    让我们分解我们对表单所做的所有更改。首先,我们移除了createPostFormRef初始化。这个引用是为了保存 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:在创建帖子表单中显示的原生验证消息

    图 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 能够实现这一点的途径是使用专门构建来为我们处理和管理表单的内置组件。

    在本节中,我们将查看三个组件:FormFieldErrorMessage

    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>)非常相似,并接受大多数你会在原生输入中使用的属性,例如占位符和名称类型。

    我们创建了两个字段。第一个字段用于电子邮件,有三个属性:idtype(它是email)和name,它将被用来将字段与其错误消息连接起来。

    第二个字段与message相关联;它与用于电子邮件的前一个字段类似,唯一的区别是有一个额外的属性称为as。这个属性用于指定字段应该渲染为哪个表单元素。当使用不带as属性的Field组件时,它默认为<input>元素。实际上,对于我们的消息,我们希望字段是一个文本区域,我们通过使用as属性并将其赋值为textarea来实现这一点。

    在这个阶段,表单已经有了使其功能所需的 HTML 和逻辑。如果我们打开我们的应用程序在http://localhost:5173/contact并填写表单,我们将在控制台看到表单正在提交:

    图 12.2:由“联系我们”表单触发的控制台消息

    图 12.2:由“联系我们”表单触发的控制台消息

    即使我们对 VeeValidate 还没有完成,你也应该能够看到它带来的好处。你可能已经意识到,我们从未需要为电子邮件和消息声明任何引用或使用v-model定义任何双向绑定。所有这些都在后台由带有FormField自定义字段的包处理。

    在本节中,我们已经将 VeeValidate 添加到我们的表单中,并学习了这将给我们的应用程序带来哪些好处。然后我们重构了我们的FieldForm组件。

    在我们完成本章之前,我们只需要对我们的表单进行最后一次修改:验证。前面的例子显示,我能够提交一个包含错误值(如伪造的电子邮件和非常简短的消息)的表单。在下一节中,我们将学习如何使用 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规则的情况下,我们使用!valuevalue.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:显示验证错误的表单

    图 12.3:显示验证错误的表单

    即使我们的错误信息显示成功,它也没有真正突出,因为它使用了与页面上的其余文本相同的颜色。这是因为 VeeValidate 提供的 ErrorMessage 组件只是一个实用组件,这意味着它实际上并不提供任何 HTML 标记,但它用于提供一些额外的功能,例如处理错误位置和可见性。

    为了改进我们的 UI,我们可以使用 as 属性来定义一个将用于包装错误信息的 HTML 元素,就像我们处理 textarea 一样,我们可以使用 "as" 属性来定义组件应该渲染为哪个 HTML 字段,并添加一些类。代码库已经为名为 error 的类提供了一些样式。所以,让我们去修改我们的 ErrorMessage 组件以使用这个类:

    <ErrorMessage as="span" name="email" class="error" />
    

    在添加 as 属性和 class 属性后,我们的组件现在将在屏幕上更加突出。

    图 12.4:显示红色错误信息的表单

    图 12.4:显示红色错误信息的表单

    表单开始成形。表单字段和标签已设置,submit 处理器已就位(即使只是一个占位符),验证规则已定义并正在工作。

    在本章的下一节和最后一节中,我们将了解 VeeValidate 提供的预设验证规则。声明一个简单的规则,如 required,很简单,但情况并不总是如此。

    我个人非常喜欢使用预设规则,因为它们帮助我保持组件小巧且易于阅读,同时仍然能够提供复杂的验证规则。

    使用 VeeValidate 规则

    到目前为止,我们只是对 email 字段应用了一个简单的验证,检查它是否已设置,但即使字段目前只需一个字符就能通过验证,这仍然会导致问题。

    在本节中,我们将验证电子邮件(以确保其格式有效)和消息文本区域(以确保我们收到的消息至少有 100 个字符)。

    我们不是通过定义其规则来手动声明验证,而是将使用 @vee-validate/rules 包,并使用其两个预设规则来实现我们的验证需求。

    VeeValidate 提供了超过 25 条规则(vee-validate.logaretm.com/v4/guide/global-validators#vee-validaterules),包括简单的规则,如 requirednumericemails,以及复杂的规则,如 one_ofnot_one_ofregex

    在学习使用这些规则所需的语法之前,我们需要在我们的应用程序中安装该包。我们可以通过运行以下命令来完成此操作:

    npm install @vee-validate/rules --save
    

    VeeValidate 和其验证的强大之处在于,它允许我们对单个字段使用多个验证,允许我们定义复杂的规则,例如 password 字段定义的规则,而无需编写任何代码。

    为了实现我们的验证需求,我们将使用emailmin规则:

    <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 提供的规则替换了它。我们声明了两个新规则,分别称为emailmin。我们使用从@vee-validate/rules导入的规则通过defineRule方法做到了这一点。

    接下来,我们在组件的<template>部分添加了我们的验证。rules属性可以接受由字符|分隔的多个条目。在email的情况下,规则是"required|email",但对于textarea,它们是"required|min:100"

    最后,我们在消息文本区域添加了ErrorMessage

    你可能已经注意到,message字段中使用的规则与email中使用的规则略有不同。这是因为min规则需要一个等于字段在标记为有效之前所需字符数的参数。为了为规则设置指定的参数,我们添加一个冒号(:)后跟一个或多个用逗号分隔的值。例如,为了定义一个max规则为164个字符,我们会写rules="max:164",而为了定义一个允许在1020之间的数字的规则,我们会写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 框架不同部分的快速洞察,这可以帮助我们完成日常任务。

    我们可以通过在我们的代码中放置 alertconsole.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 快速编辑

    监控变化

    修改组件内的数据不是开发复杂组件的唯一要求。实际上,另一个重要的功能是能够实时查看组件数据的值。例如,您可能想确保您的切换按钮能够成功工作,同时一个 Reffalse 变为 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 时间轴部分的输出,我将在我们的应用中执行以下步骤。这些步骤将生成一个报告,我们可以用它来了解时间轴提供了哪些信息:

    1. 访问时间轴标签:打开 DevTools 并点击主导航中的时间轴标签。

    2. 选择图层:我们只会使用组件事件性能Pinia图层。从图层菜单中选择它们。

    3. 开始录制:现在,我们准备好记录我们的操作。通过点击记录按钮开始录制。

    4. 屏幕上显示的某个帖子中的“删除”图标。

    5. 停止录制:再次点击记录按钮以停止录制。

    在完成前面的步骤后,时间轴应该显示类似于图 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) 或我们状态管理中可用的单个存储来查看所有存储的能力。

    信息面板中显示的数据按 stategetters 分隔,就像组件部分一样,它可以随时修改。

    就像我们迄今为止探索的大多数功能一样,能够快速查看存储的当前状态并修改它们的值非常有价值。能够随时访问这些信息将节省你无数小时的开发时间。

    vue-router Vue Devtools 插件

    如你所记,在第十章中,我们介绍了在伴侣应用中引入的路由,我们必须创建一组规则,这些规则将由路由器使用以决定向用户显示哪个路由。

    vue-router 插件可以帮助你在浏览器中直接可视化并检查这些规则。该插件提供了一个包含完整信息的路由列表,包括名称、正则表达式匹配和所需键。

    图 13.17:vue-router 规则列表

    图 13.17 中显示的列表显示了以下信息:

    • 为特定路由定义的 URL (/about)。

    • 蓝色药丸中显示的路由名称(user-profile)。

    • 当前活动路由,由带有 active 标签的蓝色药丸表示。

    • redirectexact之类的额外信息。这分别表示存在重定向规则(/user/:userId)以及当前活动路由与规则匹配正则表达式完美匹配的概念。

    现在我们已经定义了 vue-router 规则列表中可用的所有信息,是时候看看为每个单独的路由规则提供了哪些信息。让我们点击/user/:userId来查看它显示的内容:

    图片

    图 13.18:vue-router Vue Devtools 插件的路由信息

    该插件显示路由数组中可用的所有数据,例如pathnameredirect。此外,该插件还公开了更高级的数据,例如包用于决定显示正确路由所使用的正则表达式,以及关于键的详细信息,例如optionalrepeatable,最后但同样重要的是,路径中每个条目的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.js 生态系统的优质内容和最新信息,而且它们是免费的。这些时事通讯每月发送一次,非常适合周末阅读。

    如果您有时间阅读每个分享的独立帖子,您当然应该这样做,但如果您没有时间,我建议您至少花时间浏览一下帖子的名称,因为它们可以为您提供重要信息,例如主要发布或重大更改。

    社区成员

    要成为一个开发者而不加入社交媒体平台是非常困难的。我个人在 X (x.com)上以用户名@zelig880非常活跃。在本节中,我将突出一些重要的社区成员,您应该在您选择的社交媒体上亲自关注他们。

    这些人就是 Vue.js 独特之处。他们通过在会议上分享知识、成为核心存储库的活跃成员以及作为活跃的库维护者来帮助社区成长。

    我打算在每个 X 用户名旁边放置名字,以便您在网上搜索时更加方便,因为有些人可能有非常常见的名字:

    • @youyuxi): 艾文是 Vue.js 社区的杰出成员,不仅因为他创建了该框架,还因为他积极参与社区活动。艾文在会议和社交媒体上都非常活跃,并且是第一个分享即将到来的新闻的人。

    • @antfu7): 安东尼是 Vue.js 中许多库的幕后推手,包括 VueUseVitest。安东尼不仅创建了令人惊叹的库,而且还喜欢尝试新事物,因此他的时间线总是充满了美好的想法和创作。

    • @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)达到生产就绪状态,我们需要学习哪些主题。

    杂项

    以下主题对于应用达到生产就绪状态是必要的,但对于刚开始接触框架的人来说可能过于高级。

    列表中包括一些大主题,例如认证,这些主题可能需要整本书来专门介绍。我们之所以要介绍这些主题,并不是要你现在就去学习它们,而是让你意识到它们的存在,并在你需要实现这些功能的时候去学习它们。

    让我们看看我们在应用中遗漏了什么:

    • 认证:像伴侣应用这样的应用在未设置认证之前是无法完成的。幸运的是,有许多服务,如Auth0SupabaseAWS Cognito。这些工具拥有优秀的文档和入门指南。使用认证提供者是非常好的,不仅因为它能节省你的开发时间,而且因为将用户名和密码托管在服务器上是一个安全风险,也是一个复杂且你应该在职业生涯初期避免的任务。

    • 环境变量:在我们的应用程序中,我们直接在应用程序中设置了诸如令牌和 URL 之类的变量,但这是不正确的。我们应该做的是使用环境变量来安全地存储这些值,远离仓库。Vite 提供了一种简单的方式来定义环境变量(vitejs.dev/guide/env-and-mode)。

    • 日志记录/错误报告:一个应用程序在投入生产之前绝对不能没有良好的日志记录。创建一个无错误的程序是一个神话,而且你的用户遇到问题的可能性总是存在的。如今的大多数托管环境都提供了开箱即用的日志记录功能,但我更喜欢使用像Sentry这样的工具,这些工具可以帮助你快速找到应用程序中的问题,节省你的时间和金钱。

    • CI/CD:这个术语代表持续集成/持续交付。它的目的是简化并加速软件开发的生命周期。在将应用程序推广到生产状态之前,你可能想要设置一个自动的管道,快速将你的代码交付到生产环境中。幸运的是,还有一些像NetlifyVercelAWS 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-ifv-for等特性,我们能够增加我们组件的功能性,同时仍然能够编写优雅的组件。

    最后,我们介绍了computedmethods。这些功能使我们能够为我们的组件添加逻辑。

    借助到目前为止所学的知识,我们可以将一个非常静态的网站转变为一个动态且功能强大的网络应用,这将超越简单的静态 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 能够成为你们职业生涯的一个好选择。

posted @ 2025-09-08 13:04  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报