Vue3 设计模式与最佳实践(一)
原文:
zh.annas-archive.org/md5/a8e765ec91697860c03ca479c1d16d74译者:飞龙
前言
Vue 3 是“进阶框架”的最新和最强大的迭代版本,用于创建反应性和响应式的用户界面。该框架本身引入了新的概念和设计模式的方法,这在其他库和框架中可能不太常见。通过学习框架的基础知识,理解软件工程中的设计原则和模式,这本书帮助你识别每种方法的权衡,并构建稳固的应用程序。
它从基本概念开始,然后通过示例和编码场景逐步构建更复杂的架构。你将从简单的页面开始,最终构建一个多线程、离线和可安装的进阶网络应用程序(PWA)。内容还探讨了如何使用 Vue 3 的新测试工具。
不仅仅展示“如何做事”,这本书还帮助你学习如何“思考”和“处理”设计模式已经找到解决方案的常见问题。避免在每个项目中重新发明轮子将节省你的时间,并使你的软件更适合未来的变化。
本书面向的对象
本书针对关注框架设计原则并利用在 Web 应用程序开发中常见的设计模式的 Vue 开发者。学习使用和配置新的打包器(Vite)、Pinia(状态管理)、Router 4、Web Workers 和其他技术来创建高性能和稳固的应用程序。具备 JavaScript 的先验知识和 Vue 的基本知识将有所帮助。
本书涵盖的内容
第一章*, Vue* 3 框架
Vue 3 进阶框架是什么?本章介绍了框架最重要的方面和其他关键概念。
第二章*, 软件设计原则* 和模式
软件原则和模式构成了良好软件架构的标志。本章介绍了这两者,并提供了在 JavaScript 和 Vue 3 中实现示例。
第三章*, 设置* 工作项目
在建立必要的入门概念之后,本章设置了一个工作项目,该项目将作为未来项目的基准参考。它将逐步指导你如何使用正确的工具开始一个项目。
第四章*, 使用组件进行* 用户界面组合
本章介绍了用户界面的概念,并引导你进入实现一个网络应用程序的过程,从概念视觉设计到开发与之匹配的组件。
第五章*,* 单页应用程序*
这是一个关键章节,介绍了 Vue Router 以创建单页网络应用程序。
第六章*, 进阶* 网络应用程序
本章在单页应用(SPAs)的基础上构建,以创建渐进式 Web 应用(PWAs),并介绍了使用工具来评估其准备状态和性能的方法。
第七章*,数据* 流管理
本章介绍了设计和控制应用程序内以及组件间数据和信息流的关键概念。它介绍了 Pinia 作为 Vue 3 的官方状态管理框架。
第八章*,使用* Web Workers 进行多线程处理
本章重点介绍如何使用 Web Workers 进行多线程来提高大型应用性能,并介绍了更多易于实现和维护的架构模式。
第九章*,测试和* 源代码控制
在本章中,我们介绍了 Vue 团队提供的官方测试工具以及最广泛使用的版本控制系统:Git。本章展示了如何为我们的独立 JavaScript 以及 Vue 3 组件创建测试用例。
第十章*,部署* 您的应用
本章介绍了理解如何在实时生产服务器上发布 Vue 3 应用以及如何使用 Let’s Encrypt 证书对其进行安全保护所必需的概念。
第十一章*,附加章节 - UX 模式,*本附加章节深入探讨了用户界面和用户体验模式的概念,为开发者和设计师之间提供了一种共同语言。它展示了 HTML 5 标准和其他常见元素提供的常见模式。
附录:从 Vue 2 迁移到 Vue 3
本附录为经验丰富的 Vue 2 开发者提供了关于更改和迁移选项的指南。
结语
在最后一章中,作者简要总结了每章学到的所有概念,并鼓励您继续个人发展。
为了充分利用本书
本书假设您熟悉并熟悉诸如 JavaScript、HTML 和 CSS 等网络技术。对扩展他们对设计模式和架构理解感兴趣的开发者将能从本书中获得最大收益。网络应用领域的学生和初学者也可以通过仔细阅读代码示例并使用 GitHub 仓库中提供的项目来跟随本书。
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|
| 官方 Vue 3 生态系统:
-
Vue 3 框架
-
Pinia
-
Vue Router
-
Vite
-
Vitest
-
Vue 测试工具
| Windows、macOS 或 Linux | |
|---|---|
| Node.js(任何版本 + v16 LTS) | Windows、macOS 或 Linux |
| Web 服务器:NGINX、Apache | Windows 或 Linux |
| Visual Studio Code | Windows、macOS 或 Linux |
| Chrome 浏览器 | Windows、macOS 或 Linux |
考虑到现代计算机,没有特定的硬件要求,但建议至少具备以下条件:
-
至少 1 GHz 的 Intel 或 AMD CPU
-
4 GB 的 RAM(越多越好)
-
至少 10 GB 的可用存储空间(用于程序和代码)
作为一般规则,如果您的计算机可以运行现代网络浏览器(Chrome/Chromium、Mozilla Firefox 或 Microsoft Edge),那么它应该满足安装和运行本书中提到的所有开发工具的所有要求。
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件,网址为github.com/PacktPublishing/Vue.js-3-Design-Patterns-and-Best-Practices。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
代码实战
本书“代码实战”视频可在packt.link/FtCMS查看
下载彩色图像
我们还提供了一份包含本书中使用的截图和图表的彩色图像的 PDF 文件。您可以从这里下载:packt.link/oronG。
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“main.js文件将导入并启动 Vue 3 应用程序。”
代码块按以下方式设置:
<script setup>
// Here we write our JavaScript
</scrip>
<template>
<h1>Hello World! This is pure HTML</h1>
</template>
<style scoped>
h1{color:purple}
</style>
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
<script>
export default{
data(){return {_hello:"Hello World"}}
}
</script>
任何命令行输入或输出都按以下方式编写:
$ npm install
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“在这种情况下,值得提及的是童子军原则,它与它相似但适用于团队。”
小贴士或重要注意事项
看起来是这样的。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至customercare@packtpub.com,并在邮件主题中提及本书标题。
勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将非常感激您向我们报告。请访问www.packtpub.com/support/err…并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了Vue.js 3 设计模式和最佳实践,我们很乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢随时随地阅读,但无法携带您的印刷书籍到处走?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何地点、任何设备上阅读。从您最喜欢的技术书籍中搜索、复制和粘贴代码直接到您的应用程序中。
优惠远不止这些,您还可以获得独家折扣、时事通讯和每日免费内容的每日邮箱访问权限。
按照以下简单步骤获取优惠:
- 扫描下面的二维码或访问以下链接
packt.link/free-ebook/9781803238074
-
提交您的购买证明
-
就这些!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件。
第一章:Vue 3 框架
与互联网早期只是学术和科学目的的链接页面集合相比,今天的全球互联网已经发生了巨大的变化。随着技术的进步和机器变得更加强大,早期协议中添加了越来越多的功能,新技术和技术竞争,直到最终采用标准。额外的功能以浏览器插件和嵌入式内容的形式出现。Java 小程序、Flash、Macromedia、Quicktime 以及其他插件都很常见。随着 HTML5 的到来,其中大多数,如果不是全部,都逐渐被标准所取代。
现在,结构、样式和行为之间存在明确的区分。超文本标记语言(HTML)定义了构成网页的结构元素。层叠样式表(CSS)提供规则来修改 HTML 元素的显示外观,包括动画和转换。最后,JavaScript 是一种编程语言,它提供行为并可以访问和修改 HTML 和 CSS。因此,引入了众多不同的功能,也带来了浏览器之间的高度复杂性和不兼容性。这就是库和框架诞生的原因,最初是为了解决不兼容性问题并标准化外观,但很快演变为包括其他编程范式,而不仅仅是简单的 HTML 和 CSS 操作。
当今最流行的库和框架中,一些使用 响应式范式。它们巧妙地在 JavaScript 中进行更改,以自动反映在 HTML/CSS 中。Vue 3 是进阶框架的最新版本,它大量使用了响应性的概念。它还实现了其他软件设计范式和模式,允许你从静态网页中的简单交互构建到可以本地安装并可与原生桌面应用程序竞争的复杂应用程序。
在本书中,我们将探索 Vue 3 框架,并研究不同的设计模式,以帮助我们构建一流的应用程序:从简单的网页到强大的 渐进式网络应用程序(PWAs)。在这个过程中,我们将探讨软件工程中的最佳实践和经过验证的模式。
本章涵盖了以下主题:
-
进阶框架
-
单文件组件
-
不同的语法选项来编写组件
到本章结束时,你将基本了解 Vue 3 在 JavaScript 生态系统中的位置以及它提供的功能。对于 Vue 2 用户,本书附录中包含迁移应用程序时需要了解的更改。随着本书的进展,我们将在此基础上构建知识。
进阶框架
在我们描述 Vue 是什么之前,我们需要区分术语库和框架。这些术语经常被互换使用,但它们之间有一个区别,一个好的开发者应该在选择其中一个来构建 Web 应用程序时意识到这一点。
让我们来看看这些术语的定义:
-
图书馆是一个由他人开发的可重用代码集合,以函数、类等形式存在,可以轻松导入到你的程序中。它并不规定如何以及在哪里使用它,但通常,它们会提供如何使用它们的文档。程序员需要决定何时以及如何实现它们。这个概念存在于大多数开发语言中,以至于其中一些完全基于导入库来提供功能的概念。
-
框架也提供了一系列供你使用的类和函数,但规定了定义程序运行和构建方式、架构以及你的代码可以在何种条件下或如何使用的规范。这里要考虑的关键属性是,框架在应用程序中反转了控制权,因此它定义了程序的流程和数据。通过这样做,它强调了程序员应该遵守的结构或标准。
在区分了这些概念之后,现在提出了一个问题:何时使用库,何时使用框架。在回答这个问题之前,让我们明确,在构建实际应用程序时,这两者之间有一个巨大的灰色区域。从理论上讲,你可以使用任何一个来构建相同的应用程序。像软件工程中的所有事情一样,这是一个决定每种方法权衡的问题。所以,带着一点盐来接受接下来的内容;这不是刻在石头上的法律:
-
当构建小型到中型应用程序或需要向应用程序添加额外功能时(通常,你可以在框架内部使用额外的库),你可能想使用库。也有一些“大小”指南的例外。例如,React是一个库,但基于它构建了巨大的应用程序,如 Facebook。需要考虑的一个权衡是,仅使用库而不使用框架将需要建立团队内的共同方法和更多协调,因此管理和方向的努力可以显著增加。另一方面,在纯 JavaScript 编程中使用库可以提供一些重要的性能改进,并给你带来相当大的灵活性。
-
当你构建中到大型应用程序时,你可能需要使用一个框架,当你需要一个结构来帮助你协调开发,或者当你想要快速入门而跳过从头开始开发常见功能的基础。有些框架是建立在其他框架之上的,例如,Nuxt是建立在Vue之上的。需要考虑的权衡是,你被指定了一个架构模型来构建应用程序,这通常遵循特定的方法和思维方式。你和你的团队将不得不学习框架及其限制,并在这个范围内工作。总有可能你的应用程序在未来会超出框架的范围。同时,一些好处如下:更容易协调工作,从先发优势中获得相当大的收益,真正解决并经过测试的常见问题,专注于特定情况(例如,考虑购物应用与社交媒体之间的差异),等等。然而,根据框架的不同,你可能会因为额外的处理而面临一些小的性能损失或扩展困难。权衡每种情况的利弊取决于你。
那么,Vue 究竟是什么呢?根据定义,Vue 是一个渐进式框架,用于构建用户界面。渐进式意味着它具有框架的架构优势,同时也具有库的速度和模块化优势,因为特性和功能可以增量实现。在实践中,这意味着它规定了构建应用程序的某些模型,但同时也允许你从小规模开始,并根据需要扩展。你甚至可以在单个页面上使用多个 Vue 应用程序,或者接管整个应用程序。如果需要,你甚至可以导入和使用其他库和框架。相当复杂!
Vue 的另一个基本概念是响应性。它指的是自动在 HTML 中显示 JavaScript 中变量值或变化的能力,但也包括在你的代码中。这是 Vue 提供的魔法的一部分。
在传统编程中,一旦变量被分配了一个值,它就会保持这个值直到程序性地改变。然而,在响应式编程中,如果一个变量的值依赖于其他变量,那么当这些依赖项中的任何一个发生变化时,它将采用新的结果值。以下是一个简单的公式为例:
A = B + C
在响应式编程中,每当B或C的值发生变化时,A也会发生变化。正如你将在本书后面看到的,这是一个构建用户界面的非常强大的模型。在这个例子中,并且为了符合术语,A是依赖项,而B和C是依赖项。
在接下来的章节中,我们将随着构建示例应用程序来探索这个渐进式属性。但在那之前,我们需要看看 Vue 3 在其最基本形式下提供了什么。
在你的 Web 应用程序中使用 Vue
在你的 Web 应用中使用 Vue 有几种选择,这很大程度上取决于你的目标:
-
要在页面上包含一个小型自包含的应用或代码片段,你可以直接在脚本标签中导入 Vue 和代码
-
要构建一个更大的应用,你需要一个构建工具,它将你的代码打包以进行分发
注意,我使用的是“打包”这个词,而不是“编译”,因为 JavaScript 应用是在浏览器上运行时解释和执行的。这一点将在我们介绍单文件组件的概念时变得明显。
让我们简要地看看一个非常简单的 HTML 页面中的第一个案例示例:
<html>
<head>
<script src="img/vue@3"></script>
</head>
<body>
<div id="app">
{{message}}
</div>
<script>
const {createApp} = Vue
createApp({
data(){
return {message:'Hello World!'}
}
}).mount("#app")
</script>
</body>
</html>
在head部分,我们定义一个script标签并从免费的Vue导入 Vue,它暴露了框架的所有方法和函数。在我们的body标签内部,我们声明一个具有id="app"的div元素。这定义了我们的小型应用将被挂载的位置以及我们的 Vue 框架将控制的页面部分。请注意div: {{message}}的内容。双大括号定义了一个在运行时将被message变量的值替换的内容点,该变量我们在 JavaScript 中定义。这被称为插值,是值(字符串、数字等)在网页上显示的主要方式。
到了body的结尾,我们创建了一个包含我们应用的脚本元素。我们首先从 Vue 中提取createApp函数,并通过传递一个对象来使用它创建一个应用。这个对象有特定的字段,定义了一个data()方法,该方法反过来返回一个对象。这个对象中的字段名将被视为响应式变量,我们可以在 JavaScript 以及 HTML 中使用它们。最后,createApp()构造函数返回 Vue 3 应用实例,因此我们链式调用并调用mount()方法,将我们谦逊的应用挂载到具有app ID 的元素上。请注意,我们使用 CSS 选择器作为参数(井号表示id属性,因此id="app"通过#app被选中)。
由于这种使用 Vue 的方法并不常见(或流行),我们将关注更重要的事情,并使用打包器来组织我们的工作流程,并拥有显著更好的开发者体验……但首先,我们需要了解更多关于 Vue 以及是什么让它如此出色的信息。
打包器的方式,更好的方式...
如你所想,直接将 Vue 导入到网页中只会适用于非常小的应用程序。相反,Vue 以组件的概念构建,这些是可重用的、隔离的 JavaScript 代码、HTML 和 CSS 集合,它们作为一个单元运行。你可以把它们看作是构建网页的构建块。显然,浏览器对此一无所知,因此我们将使用打包器将我们的应用程序转换成浏览器可以解释的格式,同时在这个过程中还可以运行一系列优化。这就是“框架”部分开始发挥作用的地方,因为它规定了这些组件应该如何编写以及需要包含哪些方法。
使用打包器时,它将把我们的所有代码打包成一个或多个浏览器在运行时加载的 JavaScript 文件。Vue 应用程序在浏览器中的执行工作流程可以简化如下:
图 1.1:使用打包器时我们应用程序执行顺序的非常简化的视图
浏览器将像往常一样加载index.html页面,然后加载并执行bundle.js文件,就像任何其他 JavaScript 文件一样。打包器将打包所有我们的文件,并按规定的顺序执行它们:
-
main.js文件将导入并启动 Vue 3 应用程序。 -
然后,它将从主组件开始页面组合,这里封装在
App.vue文件中。这个组件将生成其他组件,从而形成一个构成页面的组件树。
如果现在听起来有点奇怪,请不要担心。随着我们在书中构建示例应用程序的进展,我们将看到这些概念的实际应用。在第三章 设置工作项目中,我们将使用这个相同的图表开始一个简单的应用程序。
到目前为止,你已经对库和框架有了了解,并且对 Vue 能提供的内容有了一个快速的浏览。重要的是要记住,在现代 JavaScript 世界中,使用打包器来帮助我们组织应用程序并对浏览器代码进行优化是很常见的。稍后我们将使用官方的 Vue 3 打包器Vite。但首先,我们需要一些更多的基础概念。
理解单文件组件
如你所猜,之前提到的App.vue文件是一个从App.vue到最后的自定义按钮的组件,如果你愿意的话。我们将在第四章中深入讨论组件,即组件的用户界面组合,但就目前而言,请记住这是框架规定的做法。如果你有面向对象语言的经验,这可能会看起来很熟悉(而且你不会错)。
SFC 是一个带有.vue扩展名的纯文本文件,包含以下部分:
<script setup>
// Here we write our JavaScript
</scrip>
<template>
<h1>Hello World! This is pure HTML</h1>
</template>
<style scoped>
h1{color:purple}
</style>
最初看起来可能有些奇怪,所有这些内容都集中在一个地方,但实际上这正是它的优点所在。以下是每个部分的描述:
-
一个
setup。这将定义我们将用于在 Vue 中编写代码的应用程序接口。我们还可以声明lang="ts"属性来使用 TypeScript 而不是纯 JavaScript。 -
一个template标签包围着我们的组件的 HTML。在这里,我们可以使用 HTML 元素、其他组件、指令等等。Vue 的一个巨大优势是我们可以使用纯 HTML 来编写我们的 HTML。这听起来可能很显然,但其他库处理这个问题完全不同,并且有自己的语法。然而,Vue 3 也允许通过使用打包器插件来使用其他语法。我们在这里也有选择。
-
一个
scoped属性,它将封装规则并限制它们只应用于我们的组件,从而防止它们“溢出”到应用程序的其他部分。与前面的部分一样,我们也可以使用不同的语法来编写样式,只要它被打包器支持。
最佳实践
总是作用域你的样式,除非你在父组件上定义样式或你希望明确传递到整个应用程序的 CSS 变量。对于应用程序范围的样式,请使用单独的 CSS 文件。
需要记住的重要概念是,一个 SFC 包含这三个定义单个组件的元素。打包器应用程序将施展其魔法,将每个部分分离并放置在适当的位置,以便浏览器可以正确地解释它们。我们将在第三章“设置工作项目”和第四章“组件的用户界面组合”中使用快速且新的Vite,深入探讨组件以及如何在它们之间处理控制流和信息流。但首先,让我们看看我们如何编写我们的组件。
不同的笔触——选项、组合和脚本设置 API
在 Vue 2 中描述组件的经典方式已被标记为Options API。为了保持向后兼容性,Vue 3 也支持相同的语法。然而,还有一个名为Composition API的新语法,这是我们将在本书中使用的。
Options API是从 Vue 2 继承的,规定一个组件由一个具有定义字段的对象定义,其中没有任何字段是强制性的。此外,其中一些具有定义的参数和预期输出。例如,这些是最常用的字段(也是一个非排他性列表):
-
data应该是一个返回对象的函数,其字段将成为响应式变量。 -
methods是一个包含我们的函数的对象。这些函数可以通过使用this.variableName格式访问data中的响应式变量。 -
components是一个对象,其中每个字段提供了一个模板的名称,值指向另一个组件(当前组件的子组件)的构造函数。 -
computed是一个对象,其属性定义了“计算”属性。每个成员随后是一个函数或对象,可以用作模板和代码中的响应式变量。函数将是只读的,对象可以包含读取和写入它们值的逻辑。这个概念将在我们查看第三章的设置工作项目中的代码示例时得到阐明。 -
props和emits声明参数以从父组件接收数据,并声明发送到父组件的事件。这为在相关组件之间进行通信和传递数据提供了一种正式的方式,但并非唯一,正如我们将在第七章的数据流管理中看到的。 -
生命周期钩子方法是一系列在组件生命周期中触发的函数。
-
混合是一种对象,它描述了可以在多个组件之间共享的通用功能。这并不是在 Vue 3 中重用代码的唯一方式。在 Options API 中使用混合引起了一些复杂性,从而产生了 Composition API。我们不会详细讨论混合,但会看到其他在组件之间共享功能的方法(例如“composables”)。
这种语法定义良好,但有一些限制。对于小型组件,它提供了过多的脚手架代码,而对于大型组件,代码组织受到严重影响,且非常冗长。此外,为了引用在 data 部分或其他方法中声明的响应式变量,内部代码必须使用 this 关键字(例如,this.data_variable_name 或 this.myMethod())。this 关键字指的是组件创建的实例。问题是当保留字 this 的含义根据使用范围和上下文而变化时。随着时间的推移,出现了其他缺点,导致了组合式 API 的创建。然而,这种语法在 Vue 3 中是相关且完全支持的。这个优点之一是你可以轻松地将代码从 Vue 2 迁移过来(在附录中稍后展示的某些考虑范围内,如附录 - 从 Vue 2 迁移)。
组合式 API 提供了一个名为 Setup() 的方法,它在组件挂载之前执行。在这个方法中,我们导入函数和组件,声明变量等,这些定义了我们的组件,而不是将它们声明为“选项”。这意味着你可以用更 JavaScript 的方式编写代码,这给了你更好的导入、重用和组织代码的自由。
让我们通过一个反应变量_hello="Hello World"来比较这两种方法:
选项式 API
<script>
export default{
data(){return {_hello:"Hello World"}}
}
</script>
组合式 API
<script>
import {ref} from "vue"
export default{
setup(){
const _hello=ref("Hello World")
return {_hello}
}
}
</script>
在选项 API 中,我们只是使用data字段来返回一个对象,其字段将变成响应式变量。Vue 将负责解释这个对象。然而,请注意,在组合式 API 中,我们首先需要从 Vue 导入ref构造函数,这将为我们创建一个响应式常量或变量。最终结果是一样的,但在这里,我们对自己的操作和位置有更多的精细控制。当使用新的 Vite 打包器时,这种对组件中导入内容的精细控制可能会导致代码构建和开发时间的加快。
初看之下,似乎组合式 API 比选项 API 更冗长,对于这样一个简单的例子确实如此。然而,随着我们的组件开始增长,这种状况就相反了。尽管如此,仍然很冗长...所以,组合式 API 有一个名为脚本设置的替代语法,这是我们将在本书中使用的语法。现在让我们比较一下使用这种新语法时组件的外观:
组合式 API – 脚本设置
<script setup>
import {ref} from "vue"
const _hello=ref("Hello World")
</script>
仅仅两行代码!这很难超越。因为我们添加了setup属性到script标签中,打包器就知道我们在这里所做的所有事情都属于组合式 API 的范畴,所有的函数、变量和常量都会自动暴露给模板。不需要定义导出。如果我们需要什么,我们可以直接导入并使用它。此外,我们现在还有一些额外的优势,如下所示:
-
我们可以在模板中显示响应式和非响应式变量
-
我们知道所有代码都是在组件挂载之前执行的
-
语法更接近 vanilla JavaScript(一个很大的优点!!!),因此我们可以根据我们的方便和愉悦来组织代码
-
更小的包大小(我之前提到过吗?是的,这很重要!)
但是等等,你可能注意到我把一个响应式变量定义为一个常量!是的,我是这样做的!而且,不,这并不是一个错误。在 JavaScript 中,常量指向一个特定的不可变值,在这个例子中,是一个对象,但这个规则只适用于对象,不适用于它的成员。ref()构造函数返回一个对象,因此常量适用于对象引用,我们可以改变其成员的值。如果你在 Java、C 或类似的语言中处理过指针,你可能认识这个概念,即使用对象的value属性。以下是一个例子:
_hello.value="Some other value";
但是,与此同时,访问这个变量在模板中的方式并没有改变:
<div>{{_hello}}</div>
简而言之,每次使用ref()构造函数将变量声明为响应式时,你需要使用constant_name.value格式来引用其值,就像模板(HTML)中的constant_name一样。当在模板中使用常量名称时,Vue 已经知道如何访问该值,你不需要像在 JavaScript 中那样显式地引用它。
小提示
采用代码约定,这样你就可以知道标识符是指变量、常量、函数、类等等
探索 Vue 3 的内置指令
Vue 还提供了称为 v- 的特殊 HTML 属性。对于本书的目的,让我们解释最常用的 Vue 指令:
v-bind: (缩写 ":")
v-bind: 指令将 HTML 属性的值绑定到 JavaScript 变量的值。如果变量是响应式的,每次它更新其值时,它都会反映在 HTML 中。如果变量不是响应式的,它将仅在 HTML 的初始渲染期间使用一次。我们通常只使用 : 缩写前缀(分号)。例如,my_profile_picture 响应式变量包含一个指向图片的网址:
<``img :src="img/my_profile_picture">
src 属性将接收 my_profile_picture 变量的值。
v-show
这个指令将显示或隐藏元素,而不会将其从文档中移除。它相当于修改 CSS 的 display 属性。它期望一个提供布尔值的变量(或可以解释为真或非空的东西)。例如,loading 变量有一个布尔值:
<``div v-show="loading">…</div>
当 loading 变量为真时,div 将会显示。
重要的是要记住,v-show 将使用对象的样式来显示或隐藏它,但元素仍然是 文档对象模型(DOM)的一部分。
v-if, v-else, 和 v-else-if
这些指令的行为与 JavaScript 中的条件语句预期一致,根据传递的表达式解析的值显示或隐藏元素。在它们将元素显示或隐藏的意义上,与 v-show 类似,但不同之处在于它们会完全从 DOM 中移除元素。因此,如果在大规模上不正确地使用经常切换状态的元素,这可能会在计算上非常昂贵,因为框架必须执行更多操作来操作 DOM,而与 v-show 不同,当只需要更改显示样式时。
注意
使用 v-if 来显示或显示那些一旦显示或隐藏后不会切换的元素(并且在初始状态为隐藏时首选)。如果元素将经常切换状态,请使用 v-show。这将提高显示大量元素时的性能。
v-for 和 :key
这两个属性结合使用时,在 JavaScript 中表现得像 for 循环。它们将根据迭代器中指定的数量创建元素副本,每个副本都有相应的插值值。这对于显示数据项集合非常有用。:key 属性在内部用于更有效地跟踪变化,并且必须引用正在迭代的项的唯一属性——例如,对象的 id 字段,或者当索引不会改变时数组中的索引。以下是一个示例:
<span v-for="i in 5" :key="i"> {{``i}} </span>
这将在网页上显示五个 span 元素,i 的插值显示以下内容:
1 2 3 4 5
v-model
这个指令简直就是魔法。当它附加到输入元素(input、textarea、select 等)上时,它将把 HTML 元素返回的值赋给引用的变量,从而保持 DOM 和 JavaScript 状态的一致性——这被称为双向绑定。以下是一个示例:
<input type="text" v-model="name">
当用户在 HTML 中输入文本时,JavaScript 中的"name"变量将立即被赋予该值。在这些示例中,我们使用的是原始数据类型,如数字和字符串,但我们也可以使用更复杂的数据类型,如对象或数组。更多内容将在第四章中介绍,即使用组件进行用户界面组合,届时我们将深入探讨组件。
v-on:(以及缩写@)
这个指令的行为与之前看到的不同。它期望的不是变量,而是一个函数或表达式,并将一个 HTML 事件绑定到一个 JavaScript 函数上以执行它。事件需要在冒号后立即声明。例如,为了响应按钮上的click事件,我们会写出以下内容:
<button v-on:click="printPage()">Print</button>
当按钮触发click事件时,JavaScript 中的"printPage()"函数将被执行。此外,这个指令的缩写更常用,从现在起,我们将在这本书中使用它:只需将v-on:替换为@。然后,之前的示例变为以下内容:
<button @click="printPage()">打印</button>
您可以在官方文档中找到内置指令的完整列表:vuejs.org/api/built-in-directives.html。随着我们的前进,我们将看到其他指令。
到目前为止,我们已经看到 Vue 3 应用程序是通过组件构建的,这些组件我们可以用在我们的 HTML 中,并且我们使用 SFCs 创建它们。该框架还为我们提供了用于操作 HTML 元素的指令,但这并不是全部。在下一节中,我们将看到该框架还提供了一些方便的预构建组件供我们使用。
内置组件
该框架还为我们提供了几个内置组件,我们可以在不将它们显式导入每个 SFC 的情况下使用。我在这里为每个组件提供了一个简短的描述,因此您可以参考官方文档以获取语法和示例(见vuejs.org/api/built-in-components.html):
-
Transition和TransitionGroup是两个可以协同工作以提供元素和组件动画和过渡的组件。它们需要您创建 CSS 动画和过渡类,以便在将元素插入或从页面中删除时实现动画。它们主要(或经常)用于您使用v-for/:key或v-if/v-show指令显示元素列表时。 -
KeepAlive是另一个包装组件(意味着它包围其他组件),用于在包裹的组件不再显示时保留状态(内部变量、元素等)。通常,组件实例在卸载时会被清除并“垃圾回收”。KeepAlive将它们缓存起来,以便它们在重新显示时恢复状态。 -
Teleport是 Vue 3 中的一个全新的组件,允许你将组件的 HTML 传输到页面上的任何位置,甚至可以传输到应用组件树之外。这在某些情况下很有帮助,当你需要在外部显示信息但必须由组件的内部逻辑处理时。 -
Suspense是 Vue 3 中的一个新组件,但仍然处于实验阶段,因此在撰写本文时其未来尚不确定。其基本思想是在所有异步子组件/元素准备好渲染之前显示“后备”内容。它作为一个便利性提供,因为存在可以用来解决这个问题的一些模式。我们稍后会看到一些。 -
Component-is是一个特殊元素,它将在运行时根据变量的内容加载组件——例如,如果我们需要根据变量的值显示一个组件,而使用其他指令可能很繁琐。它也可以用来渲染 HTML 元素。让我们看一个例子:<script setup> import EditItem from "EditItem.vue" import ViewItem from "ViewItem.vue" import {ref} from "vue" const action=ref("ViewItem") </script> <template> <component :is="action"></component> <button @click="action='EditItem'">Edit</button> </template>
在这个简单的例子中,当用户点击“编辑”按钮时,动作值将更改为EditItem,并且组件将在原地交换。你可以在这里找到文档:vuejs.org/api/built-in-special-elements.html.
基于框架和组件的理念,我们现在更好地准备向前迈进。
书籍代码约定
在这本书中,我们将使用一组适用于 Vue 3 的代码约定和指南,这些都是良好的实践。它们将帮助你不仅理解本书的示例,还能理解你可能会遇到的野外科普代码,因为越来越多的开发者正在使用它。让我们从开始讲起。
变量和属性
这些名称总是小写,并且空格被下划线替换,例如 total_count 和 person_id..
常量
对注入对象的引用以 $(美元符号)开头,例如 $router、$modals 和 $notifications。
对响应式数据的引用以 _ 开头,并使用蛇形命名法,例如 _total 和 _first_input。
对常量值的引用全部使用大写字母,例如 OPTION 和 LANGUAGE_CODE.
注入依赖的构造函数将以 use 开头,例如 const $store=useStore().
类和组件名称
这些名称使用 PascalCase(每个单词以大写字母开头),例如 Person、Task 和 QueueBuilder。
函数、方法、事件和文件名
例如,这些是用驼峰命名法编写的,例如,doSubscribe()和processQueue()
实例
实例将具有抽象名称,对于提供函数的纯 JavaScript 对象,后面跟着单词Service,对于状态模型,则是Model,等等。我们将使用服务来封装功能。
这里有一个例子:const projectService=new ProjectService().
小贴士
与你的团队一起,始终使用大家都同意的代码约定。这将使代码更易于阅读和维护。还建议使用一个 linter(一个用于捕获你代码中约定的处理器)。
正如之前提到的,这些代码约定越来越受欢迎,所以你可能会在多个项目中看到它们。然而,这些并不是强制性的标准,绝对不是由框架规定的。如果你喜欢全部大写,那也可以,但真正重要的是你和你的团队能够以一致的方式定义并遵守自己的约定。最终,重要的是我们都有一种共同的代码编写语言。
摘要
本章已经从库和框架的基础知识过渡到 Vue 3 指令、组件,甚至代码约定。这些概念仍然有些抽象,所以随着我们继续阅读本书的其余部分并编写实际代码,我们将把它们具体化。然而,我们现在已经安全地站在了学习下一章设计原则和模式的基础上。
复习问题
为了帮助你巩固本章内容,你可以使用以下复习问题:
-
库和框架之间的区别是什么?
-
为什么 Vue 是一个“渐进式”框架?
-
单文件组件是什么?
-
在 Vue 开发中最常用的指令有哪些?
-
代码约定为什么很重要?
如果你能在脑海中迅速回答这些问题,那么你已经准备好了!如果不能,你可能需要简要回顾本章,以确保你具备继续前进的基础。
第二章:软件设计原则和模式
软件开发本质上是一门人密集型学科。这意味着它需要了解技术和技术,同时也需要理解问题和在多个抽象层次上实施解决方案的决策能力。编程与开发者的思维方式有很大关系。多年来,在每个上下文和语言中,都出现了解决重复问题的指南和解决方案。了解这些模式将帮助您确定何时应用它们,并确保您的开发工作稳步推进。另一方面,原则是指导概念,应在过程的每个阶段应用,并且更多关乎您如何处理这个过程。
在本章中,我们将探讨在 Vue 3 应用程序开发中常见的非排他性和非详尽性的原则和模式列表。
| 原则 | 模式 |
|---|
|
-
关注点分离
-
组合优于继承
-
单一职责
-
封装
-
KIC – 保持清洁
-
DRY – 不要重复自己
-
KISS – 简单就是聪明
-
为未来编写代码
|
-
单例
-
依赖注入
-
观察者
-
命令
-
代理
-
装饰者
-
门面
-
回调
-
承诺
|
表 2.1 – 本章涵盖的原则和模式
理解这些原则和模式将帮助您更有效地使用框架,并且通常情况下,它将防止您“重新发明轮子”。与第一章一起,这将结束本书的基础部分,并为您跟随本书剩余部分的实际部分和应用示例的实现提供基础。
软件设计原则是什么?
在软件开发中,设计原则是适用于整个过程的宏观概念性指南。并非每个项目都会使用相同的原理,这些也不是必须强制执行的规则。它们可以从架构到用户界面(UI)和最后一段代码出现在项目中。在实践中,这些原则中的一些也可以影响软件属性,如可维护性和可重用性。
设计原则的非排他性列表
设计原则因上下文、领域,甚至一个人可能参与的团队而异。因此,本章包含的原则是非排他性的。
关注点分离
这可能是软件工程中最重要的原则。关注点的分离意味着一个系统必须被划分为由功能或服务(即关注点)组成的子系统。例如,我们可以将人体视为由许多子系统(呼吸、循环、消化等)组成的系统。这些子系统再由不同的器官整合,器官由组织构成,以此类推,直至最小的细胞。在软件中遵循同样的理念,一个应用程序可以被划分为按关注点分组的不同元素,从大型架构一直到最后的函数。如果没有将复杂性分解为可管理的部分,创建一个功能系统将会更加困难,甚至不可能。
通常,这一原则的应用从系统应该是什么的大图景开始,考虑它应该做什么来实现这一点,然后将它分解为可管理的可工作部分。
例如,这里是一个关于 Web 应用程序关注点分离的粗略图形表示。这个图中的每个框都标识了一个不同的关注点,这些关注点反过来又可以细分为更小的功能部分。更好的是,你可以看到这一原则如何帮助你识别系统的整合部分。
图 2.1 – 一个简单的 Web 应用程序架构视图,展示了关注点的分离
如果我们要深入到各自领域内的任何这些小框中,我们仍然可以找到更多需要进一步细分的关注点,直到达到不可分割的原子元素(例如组件或函数)。这一原则与许多其他原则有很大关系,并从中受益,如抽象和单一职责。我们将在本章的后面进一步讨论它们。
组合优于继承
组合优于继承的原则直接来源于面向对象编程(OOP)。它指出,一个对象在需要时应该尝试使用其他对象的功能,通过引用或实例化它们,而不是创建一个庞大而复杂的继承家族树来添加这样的功能。现在,JavaScript 本质上是一种函数式语言,尽管它支持多种范式,包括来自 OOP 的特性,所以这一原则同样适用。对于那些从 OOP 迁移到 JavaScript 的人来说,有一个警告需要注意,那就是避免将 JavaScript 视为纯粹的 OOP 语言。这样做可能会创造不必要的复杂性,而不是从语言的优点中受益。
在 Vue 3 中,没有组件的扩展或继承。当我们需要共享或继承功能时,我们有一套很好的工具集来替代继承范式。我们将在第四章“用户界面组件组合”中看到如何通过使用组合组件来遵守这一原则。
单一职责原则
这一原则在面向对象编程以及函数式编程中都可以找到。简单来说,它指出一个类、方法、函数或组件应该只处理一个职责或功能。如果你在其他学科和语言中工作过,这会自然而然地发生。多功能函数难以维护,并且往往会失去控制,尤其是在像 JavaScript 这样松散类型和高度动态的语言中。同样的概念也直接适用于 Vue 3 组件。每个组件应该处理一个特定的操作,避免试图自己完成太多。在实践中,当一个组件超出一定范围时,最好将其拆分为多个组件或将行为提取到外部模块中。有时你可能会得到一个数千行长的组件,但根据我的经验,这很少是必要的,并且可以也应该避免。不过,有一个警告,即过多的具体性也可能导致不必要的复杂性。
例如,让我们想象一个同时显示注册选项的登录屏幕。这种做法在许多网站上都很常见。您可以将所有功能都包含在一个组件中,但这会违背这一原则。更好的选择是将组件拆分为至少三个组件来完成这项任务:
-
处理 UI 逻辑的父组件。该组件决定何时显示/隐藏登录和注册组件。
-
处理登录功能的子组件。
-
处理注册功能的子组件。
这里是这个配置的图形表示:
图 2.2 – 使用多个组件组合的登录/注册界面
我认为你可以很快地理解这一原则的好处。它使得代码易于管理、维护和适应,因为网络应用有快速变异和演化的趋势。
最佳实践技巧
给组件赋予单一职责和功能。尽可能避免庞大的单体组件。
封装
封装是指你应该将数据和函数包装成一个单一单元,同时暴露一个定义良好的应用程序编程接口(API)。通常,这以类、模块或库的形式完成。JavaScript 也不例外,强烈建议遵循这个原则。在 Vue 3 中,这个概念不仅适用于组件,也适用于 CSS 样式和 HTML。单文件组件的引入是框架如何在实际中促进这个原则的一个明显例子,以及它对当今开发的重要性。在只有少数边缘情况的情况下,我们应该将(UI)组件视为接收传入参数并提供输出数据的黑盒,其他组件不应了解它们的内部工作方式,只有 API。随着我们在本书中构建示例应用程序,你将看到这个原则是如何发挥作用的。
KIC – 保持清洁
这个原则主要指的是你编写代码的方式。我应该在这一点上强调,KIC 直接应用于两个强烈影响 Web 和 Vue 3 应用程序的类别:
-
你如何格式化你的代码
-
你如何整理事件和变量
第一项包括使用代码约定、注释和缩进来组织代码以及函数的逻辑分组。例如,如果你有处理创建、读取、更新和删除(CRUD)操作的方法,最好将它们放在代码的附近,而不是分散在源文件中。许多集成开发环境(IDE)包含折叠或展开函数内部代码的功能。这有助于快速审查和定位具有相似逻辑的代码部分。
这个原则的第二部分与内存和引用处理有关。JavaScript 有一个非常好的垃圾回收器,其功能是丢弃未使用的数据以回收内存。然而,有时算法因为引用仍然挂起而无法释放资源。如果你使用过其他语言,如 C/C++,这个问题可能听起来很熟悉,因为你需要在不再使用时手动分配和释放内存。在 JavaScript 中,如果你注册一个函数来监听一个事件,当不再需要时,最好在你的组件适当的生命周期事件中手动注销它。这将防止内存泄漏和内存浪费,同时也防止一些安全风险(这些风险超出了本书的范围)。
我们将在第四章**,使用组件的用户界面组合中回顾组件的生命周期,但到目前为止,以下示例是这一原则的良好应用,并作为最佳实践保留。在这个例子中,我们将创建一个可组合组件来检测窗口大小的变化,因此,在script setup部分,我们会找到如下内容:
-
在挂载状态下,在窗口对象的 resize 事件上注册一个函数。
-
在组件卸载之前注销事件。
这里是代码片段:
<script setup>
import {onMounted, onBeforeUnmount} from "vue"
onMounted(()=>{
window.addEventListener("resize", myFunction)
})
onBeforeUnmount(()=>{
window.removeEventListener("resize", myFunction)
})
function myFunction(){
// Do something with the event here
}
</script>
onMounted和onBeforeUnmount函数是 Vue 3 框架的一部分,并由适当的组件生命周期事件触发。在这里,当组件挂载到文档对象模型(DOM)时,我们将函数附加到resize事件上,并在它被移除之前释放它。需要记住的重要概念是清理自己的工作并保持其整洁。
DRY – 不要重复自己
这个原则相当有名,几乎到了变成陈词滥调的地步。遗憾的是,它很容易被遗忘。它归功于安德鲁·亨特和大卫·托马斯,他们在《实用程序员》一书中使用了它。它主要被认为是“不要重复写同一件事”,虽然接近,但它的含义更广。它包括在过程以及应用程序的逻辑中避免冗余的概念。核心思想是,执行业务逻辑的每个过程应该只存在于整个应用程序的一个地方。
例如,大多数 Web 应用程序都通过 API 使用与服务器的一些异步连接。应用程序中可能还有多个元素将使用或需要使用这种远程计算机/服务器通信。如果你打算在每个组件中编写与服务器通信的整个代码/逻辑,我们最终会得到代码重复以及应用程序逻辑。维护这样的系统会打开通往大量负面副作用和安全问题的门,包括糟糕的用户体验等等。根据这个原则,更好的方法是将与服务器 API 相关的所有通信代码抽象成一个单独的模块或类。在实践中,在 JavaScript 中,这甚至可以委托给一个单独线程中的 Web Worker。我们将在第八章,使用 Web Workers 进行多线程中探讨这种实现。
作为一条经验法则,如果你发现自己正在不同的组件或类中编写“有点相似”的代码,那么将功能抽象成其自己的模块或组件是一个明显的机遇。
KISS – 保持简单和简洁
这个原则并不仅限于软件开发领域。它是在 20 世纪 60 年代由美国海军提出的(根据维基百科,en.wikipedia.org/wiki/KISS_principle)。这个想法纯粹是常识:构建简单、小巧且能协同工作的功能部件,比一次性尝试创建一个庞大而复杂的程序要好。此外,算法应以最简单和最有效的方式进行实现。在 Web 开发中,这个原则至关重要。现代 Web 应用程序由数百个工作部件组成,这些部件分布在多个计算机、服务器和环境上。系统或代码实现越复杂,维护和适应的难度也越大。
虽然有一个警告。保持简单并不意味着过度简化或不必要的隔离。太多的部分可能会在系统中引入不必要的复杂性。应用 KISS 原则意味着保持在那个事物可管理和易于理解的美好中间点。
为未来编写代码
这个原则是指你应该让你的代码对除了你自己之外的其他人来说也是可读和易于理解的。命名约定、逻辑流程和行间注释都是这个原则的一部分。这不仅是为了在你可能需要将代码委托给其他人时,也是为了当你一年或两年后回到相同的代码时。你不想做的事情就是浪费时间思考过去那个缺乏经验的你用那行巧妙的意大利面代码做了什么。聪明的开发者编写代码就像他们要教给别人一样,简单而优雅。特别是如果你在使用或为开源代码做出贡献,这个原则对于团队协作至关重要。在这种情况下,值得提到的是童子军原则,它与前者类似,但适用于团队。它指出,当你发现难以阅读或“意大利面”代码时,你应该重构它以使其变得干净。
最佳实践技巧
使用源代码注释和文档来解释你的逻辑,保持你的代码干净,就像在教别人一样。大多数情况下,你实际上是在教自己。
设计原则适用于许多不同的场景,一些场景甚至超出了软件开发实践。考虑它们直到它们成为第二天性是很重要的。一般来说,这些原则以及其他原则的应用,以及设计模式的应用,对你的职业发展留下了重要的影响。
什么是软件设计模式?
在软件开发中,某些流程和任务以某种方式或某种程度的变化出现在多个项目中是很常见的。设计模式是解决此类类似问题的有效解决方案。它不规定代码,而更像是一个推理模板,一种独立于实现进行抽象、可重用和适应特定情况的方法。在实践中,有足够的空间发挥创意来应用模式。已经有许多书籍致力于这个主题,并提供了比本书范围更详细的信息。在接下来的几页中,我们将探讨我认为对于 Vue 3 应用程序来说最常见且需要记住的模式。尽管我们为了研究它们而单独看待它们,但现实情况是,通常实现会重叠、混合和封装多个模式在一个代码块中。例如,你可以使用单例来充当装饰器和代理,以简化或改变应用程序中服务之间的通信(我们实际上会这样做很多次,完整的代码可以在第八章**,使用 Web Workers 进行多线程 中查看)。
设计模式也可以理解为软件工程和开发最佳实践。而与之相反的,不良实践通常被称为反模式。反模式是“解决方案”,尽管它们在短期内解决了问题,但沿着这条线会引发问题和不良后果。它们产生了绕过问题的需要,并使整个结构和实现不稳定。
现在让我们查看一个列表,这些模式应该是 Vue 3 项目工具箱的一部分。
模式快速参考列表
模式根据它们解决的问题或功能类型进行分类。根据系统的上下文、语言和架构,有许多模式。以下是我们将在本书中使用的模式列表,以及根据我的经验,这些模式更有可能在 Vue 应用程序中出现:
-
创建型模式:这些处理创建类、对象和数据结构的方法:
-
单例模式
-
依赖注入模式
-
工厂模式
-
-
行为模式:这些处理应用程序中对象、组件和其他元素之间的通信:
-
观察者模式
-
命令模式
-
-
结构模式:这些提供模板,影响应用程序的设计和组件之间的关系:
-
代理模式
-
装饰器模式
-
门面模式
-
-
异步模式:这些模式处理单线程应用程序(在 Web 应用程序中大量使用)中的异步请求和事件的数据和流程:
-
回调模式
-
承诺模式
-
无论如何,这个模式列表并不是唯一的。还有许多其他模式和分类,一个完整的库专门用于这个主题。值得一提的是,这些描述和应用可能因文献而异,并且根据上下文和实现可能存在一些重叠。
在介绍完设计模式之后,让我们通过示例来详细探讨它们。
单例模式
这是在 JavaScript 中非常常见的一种模式,也许是最重要的一种。基本概念定义了一个对象实例在整个应用程序中只能存在一次,所有的引用和函数调用都通过这个对象进行。单例可以作为资源、库和数据的网关。
何时使用
这里有一个简单的规则,可以帮助您了解何时应用此模式:
-
当您需要确保资源只通过一个网关访问时,例如,全局应用程序状态
-
当您需要封装或简化行为或通信(与其他模式结合使用时)。例如,API 访问对象。
-
当多次实例化的 成本 有害时。例如,创建网络工作者。
实现
您可以在 JavaScript 中以多种方式应用此模式。在某些情况下,从其他语言迁移的实现会遵循 Java 示例,通常使用 getInstance() 方法来获取单例。然而,在 JavaScript 中实现此模式有更好的方法。让我们看看下面的例子。
方法 1
最简单的方法是通过导出一个普通的对象字面量或 JavaScript 对象表示法(JSON),这是一个静态对象:
./chapter 2/singleton-json.js
const my_singleton={
// Implementation code here...
}
export default my_singleton;
您可以将此模块导入其他模块,并且始终拥有相同的对象。这是因为打包器和浏览器足够智能,可以避免重复导入,所以一旦这个对象第一次被引入,它将忽略后续的请求。当不使用打包器时,JavaScript 的 ES6 实现也定义了模块是单例的。
方法 2
此方法创建一个类,然后在第一次实例化时保存对未来的引用。为了使此方法生效,我们使用类中的一个变量(传统上称为 _instance)并在构造函数中保存对实例的引用。在后续调用中,我们检查 _instance 值是否存在,如果存在,则返回它。以下是代码:
./chapter 2/singleton-class.js
class myClass{
constructor(){
if(myClass._instance){
return myClass._instance;
}else{
myClass._instance=this;
}
return this;
}
}
export default new myClass()
第二种方法可能对其他语言开发者来说更为熟悉。注意我们也是导出一个新的类实例,而不是直接导出类。这样,调用者就不必每次都记住实例化类,代码将与 方法 1 中的代码相同。这种情况需要与您的团队协调,以避免不同的实现。
调用者可以直接调用每个对象的方法(假设单例有一个名为 myFunction() 的函数/方法):
./chapter 2/singleton-invoker.js
import my_method1_singleton from "./singleton-json";
import my_method2_singleton from "./singleton-class";
console.log("Look mom, no instantiation in both cases!")
my_method1_singleton.myFunction()
my_method2_singleton.myFunction()
单例模式非常有用,尽管它很少独立存在。通常,我们使用单例来封装其他模式的实现,并确保我们有一个单一的访问点。在我们的示例中,我们将经常使用这个模式。
依赖注入模式
这个模式简单地声明,一个类或函数的依赖项作为输入提供,例如作为参数、属性或其他类型的实现。这个简单的声明打开了一个非常广泛的可能性。以一个与浏览器的 dbManager.js 文件一起工作的类为例,它公开了一个处理数据库操作的对象,而 projects 对象处理项目表的 CRUD 操作(或集合)。如果不使用依赖注入,你将得到类似这样的结果:
./chapter 2/dependency-injection-1.js
import dbManager from "dbManager"
const projects={
getAllProjects(){
return dbManager.getAll("projects")
}
}
export default projects;
上述代码展示了“正常”的方法,即在文件开头导入依赖项,然后在我们的代码中使用它们。现在,让我们调整相同的代码以使用依赖注入:
./chapter 2/dependency-injection-2.js
const projects={
getAllProjects(dbManager){
return dbManager.getAll("projects")
}
}
export default projects;
如您所见,主要区别在于现在将 dbManager 作为参数传递给函数。这就是所谓的注入。这为依赖项管理开辟了许多途径,同时将依赖项的硬编码推到实现树的更高层次。这使得这个类非常易于重用,至少在依赖项遵守预期 API 的情况下是这样。
上述示例并不是注入依赖的唯一方式。例如,我们可以将其分配给对象的内部属性。例如,如果 projects.js 文件使用属性方法实现,它将看起来像这样:
./chapter 2/dependency-injection-3.js
const projects={
dbManager,
getAllProjects(){
return this.dbManager.getAll("projects")
}
}
export default projects;
在这种情况下,对象的调用者(顺便提一下,是一个单例)需要知道这个属性,并在调用任何函数之前将其分配。以下是一个示例:
./chapter 2/dependency-injection-4.js
import projects from "projects.js"
import dbManager from "dbManager.js"
projects.dbManager=dbManager;
projects.getAllProjects();
但这种方法并不推荐。你可以清楚地看到,它打破了封装的原则,因为我们直接为对象分配了一个属性。尽管它是有效的代码,但它看起来并不像是整洁的代码。
逐个传递依赖项也不是推荐的做法。那么,更好的方法是什么呢?这取决于实现方式:
-
在一个类中,在构造函数中要求依赖项(如果找不到,则抛出错误)是很方便的。
-
在一个普通的 JSON 对象中,提供一个函数来显式设置依赖项,并让对象决定如何内部使用它是很方便的。
最后一种方法也推荐在对象实例化后传递依赖项,当依赖项在实现时未准备好时使用。
以下是对前面列表中提到的第一点的一个代码示例:
./chapter 2/dependency-injection-5.js
class Projects {
constructor(dbManager=null){
if(!dbManager){
throw "Dependency missing"
}else{
this.dbManager=dbManager;
}
}
}
在构造函数中,我们声明一个具有默认值的预期参数。如果未提供依赖项,我们抛出错误。否则,我们将它分配给实例的一个内部私有属性以供使用。在这种情况下,调用者应该如下所示:
// Projects are a class
import Projects from "projects.js"
import dbManager from "dbManager.js"
try{
const projects=new Projects(dbManager);
}catch{
// Error handler here
}
在另一种实现中,我们可以有一个函数,它基本上通过接收依赖并将其分配给一个私有属性来完成相同的功能:
import projects from "projects.js"
import dbManager from "dbManager.js"
projects.setDBManager(dbManager);
这种方法比直接分配内部属性更好,但你仍然需要记住在使用对象中的任何方法之前进行分配。
最佳实践提示
无论你使用什么方法进行依赖注入,都要在整个代码库中保持一致。
你可能已经注意到,我们主要关注的是对象。正如你可能已经猜到的,将依赖项传递给函数与传递另一个参数是一样的,所以它不值得特别注意。
这个例子只是将依赖实现的责任移动到层次结构中的另一个类。但如果我们实现一个单例模式来处理我们应用程序中的所有或大部分依赖呢?这样,我们就可以在应用程序生命周期中的某个确定点将依赖的加载委托给一个类或对象。但我们应该如何实现这样的功能?我们需要以下内容:
-
注册依赖的方法
-
通过名称检索依赖项的方法
-
一个结构来保持对每个依赖项的引用
让我们将其付诸实践,创建一个非常天真的单例实现。请记住,这是一个学术练习,所以我们不考虑错误检查、注销或其他考虑因素:
./chapter 2/dependency-injection-6.js
const dependencyService={ //1
dependencies:{}, //2
provide(name, dependency){ //3
this.dependencies[name]=dependency //4
return this; //5
},
inject(name){ //6
return this.dependencies[name]??null; //7
}
}
export default dependencyService;
在这个最基本实现的基础上,让我们逐行通过注释来看:
-
我们创建一个简单的 JavaScript 对象字面量作为单例。
-
我们声明一个空对象,用作字典来按名称存储我们的依赖项。
-
provide函数让我们可以通过名称注册依赖项。 -
在这里,我们只使用名称作为字段名,并分配通过参数传递的依赖项(注意我们没有检查预存在的名称等)。
-
在这里,我们返回源对象,主要是为了方便,这样我们就可以链式调用。
-
inject函数将接受在provide函数中注册的名称。 -
我们返回依赖项或
null(如果未找到)。
在有了这个单例之后,我们现在可以在整个应用程序中使用它,按需分配依赖项。为此,我们需要一个父对象来导入它们并填充服务。以下是一个示例,说明这可能看起来像什么:
./chapter 2/dependency-injection-7.js
import dependencyService from "./dependency-injection-6"
import myDependency1 from "myFile1"
import myDependency2 from "myFile2"
import dbManager from "dbManager"
dependencyService
.provide("dependency1", myDependency1)
.provide("dependency2", myDependency2)
.provide("dbManager", dbManager)
正如你所见,这个模块有硬编码的依赖项,它的作用是将它们加载到 dependencyService 对象中。然后,依赖的函数或对象只需要导入服务,并通过注册名称检索所需的依赖项,如下所示:
import dependencyService from "./dependency-injection-6"
const dbManager=dependencyService.inject("dbManager")
这种方法确实在组件之间创建了一个紧密的耦合,但这里提供它作为参考。它的优点是我们可以在一个位置控制所有的依赖项,这样维护的益处可能是显著的。dependencyService 对象的方法名称的选择也不是随机的:这些名称与 Vue 3 在组件层次结构内部使用的名称相同。这对于实现一些用户界面设计模式非常有用。我们将在第四章,使用组件进行用户界面组合和第七章,数据流管理中更详细地看到这一点。
正如你所见,这种模式非常重要,并且在 Vue 3 中通过 provide/inject 函数实现。这是对我们工具集的一个很好的补充,但还有更多。让我们继续下一个。
工厂模式
工厂模式为我们提供了一种创建对象而不直接创建依赖项的方法。它通过一个函数来实现,该函数根据输入将返回一个实例化的对象。这种实现的用法将通过一个公共或标准接口进行。例如,考虑两个类:Circle 和 Square。这两个类都实现了相同的 draw() 方法,该方法将图形绘制到画布上。然后,一个 factory 函数将类似于这样:
function createShape(type){
switch(type){
case "circle": return new Circle();
case "square": return new Square();
}}
let
shape1=createShape("circle"),
shape2=createShape("square");
shape1.draw();
shape2.draw();
这种方法相当流行,尤其是在与其他模式结合使用时,正如我们将在本书中多次看到的。
观察者模式
观察者模式非常有用,是响应式框架的基础之一。它定义了对象之间的关系,其中一个对象(主题)被观察以检测变化或事件,而其他对象(观察者)则被通知这些变化。观察者也被称为监听器。以下是它的图形表示:
图 2.3 – 主题对象发出事件并通知观察者
正如你所见,主题对象会发出事件来通知观察者。主题对象需要定义它将发布哪些事件和参数。同时,观察者通过向发布者注册一个函数来订阅每个事件。这种实现方式使得这种模式通常被称为发布/订阅模式,并且它可以有多种变体。
在考虑实现此模式时,重要的是要注意发布的基数:1 个事件对应 0..N 个观察者(函数)。这意味着主题必须在它的主要目的之上实现发布事件和跟踪订阅者的功能。由于这会打破设计中的几个原则(关注点分离、单一责任等),通常会将此功能提取到一个中间对象中。因此,先前的设计变为添加一个中间层:
图 2.4 – 带有调度器中间对象的观察者实现
这个中间对象,有时被称为“事件调度器”,封装了注册观察者、从主题接收事件并将它们分发给观察者的基本功能。当观察者不再观察时,它还会执行一些清理活动。让我们将这些概念应用到纯 JavaScript 中的简单且原始的事件调度器实现中:
./chapter 2/Observer-1.js
class ObserverPattern{
constructor(){
this.events={} //1
}
on(event_name, fn=()=>{}){ //2
if(!this.events[event_name]){
this.events[event_name]=[]
}
this.events[event_name].push(fn) //3
}
emit(event_name, data){ //4
if(!this.events[event_name]){
return
}
for(let i=0, l=this.events[event_name].length; i<l; i++){
this.events[event_name]i
}
}
off(event_name, fn){ //5
let i=this.events[event_name].indexOf(fn);
if(i>-1){
this.events[event_name].splice(i, 1);
}
}
}
上述实现再次是原始的。它不包含在生产环境中使用的必要错误和边缘情况处理,但它确实为事件调度器提供了基本的基本功能。让我们逐行查看它:
-
在构造函数中,我们声明一个对象,将其用作内部字典来存储我们的事件。
-
on方法允许观察者注册他们的函数。在这一行中,如果事件尚未初始化,我们创建一个空数组。 -
在这一行中,我们只是将函数推送到数组中(正如我所说的,这是一个原始的实现,因为我们没有检查重复,例如)。
-
emit方法允许主题通过其名称发布事件并向其传递一些数据。在这里,我们遍历数组并执行每个函数,传递我们接收到的作为参数的数据。 -
off方法是必要的,以便在不再使用函数时取消注册(参见本章早些时候提到的 保持清洁 原则)。
为了使此实现工作,每个观察者和主题都需要引用相同的 ObserverClass 实现。最简单的方法是通过 单例模式 来实现它。一旦导入,每个观察者都会使用以下行向调度器注册:
import dispatcher from "ObserverClass.js" //a singleton
dispatcher.on("event_name", myFunction)
然后,主题通过以下行发出事件并传递数据:
import dispatcher from "ObserverClass.js" //a singleton
dispatcher.emit("event_name", data)
最后,当观察者不再需要监视主题时,它需要使用 off 方法清理与主题的引用:
dispatcher.off("event_name", myFunction)
在这里,我们没有涵盖许多边缘情况和控制,而不是重新发明轮子,我建议使用现成的解决方案来处理这些情况。在我们的书中,我们将使用一个名为mitt的解决方案(www.npmjs.com/package/mitt)。它具有与我们示例中相同的方法。我们将在第三章,设置工作项目中看到如何安装打包的依赖项。
命令模式
这个模式非常有用且易于理解和实现。而不是立即执行一个函数,基本概念是创建一个包含执行所需信息的对象或结构。这个数据包(命令)然后委托给另一个对象,该对象将根据某些逻辑执行执行。例如,命令可以被序列化并排队、调度、反转、分组和转换。以下是这个模式的图形表示,包括必要的部分:
图 2.5 – 命令模式的图形实现
该图显示了客户端如何向调用者提交他们的命令。调用者通常实现某种队列或任务数组来处理命令,然后将执行路由到适当的接收者。如果有任何数据要返回,它也会返回给适当的客户端。调用者通常还会将附加数据附加到命令中,以跟踪客户端和接收,特别是在异步执行的情况下。它还提供了一个“入口点”到接收者,并将“客户端”与它们解耦。
让我们再次尝试一个Invoker类的简单实现:
./chapter 2/Command-1.js
class CommandInvoker{
addCommand(command_data){ //1
// .. queue implementation here
}
runCommand(command_data){ //2
switch(command_data.action){ //3
case "eat":
// .. invoke the receiver here
break;
case "code":
// .. invoke the receiver here
break;
case "repeat":
// .. invoke the receiver here
break;
}
}
}
在前面的代码中,我们逐行实现了Invoker应该具备的裸骨示例:
-
Invoker提供了一个方法来向对象添加命令。这仅在命令需要以某种方式排队、序列化或根据某些逻辑处理时才是必要的。 -
这行代码根据
command_data参数中包含的action字段执行命令。 -
根据
action字段,调用者将执行路由到适当的接收者。
实现路由执行逻辑的方法有很多。重要的是要注意,这个模式可以根据上下文在更大范围内实现。例如,调用者可能甚至不在 Web 客户端应用程序中,而是在服务器或不同的机器上。我们将在第八章,使用 Web Workers 进行多线程中看到这个模式的实现,在那里我们使用这个模式在不同线程之间处理任务并卸载主线程(Vue 3 运行的地方)。
代理模式
这种模式的定义直接来源于其名称,因为“代理”一词意味着代表他人行事的人或事物,仿佛它就是同一个。这听起来有点复杂,但它会帮助你记住它。让我们通过一个例子来了解它是如何工作的。我们需要至少三个实体(组件、对象等):
-
一个需要访问目标实体 API 的客户端实体
-
一个暴露了知名 API 的目标实体
-
一个位于中间并暴露与目标相同 API 的同时拦截来自客户端的每条通信并将其转发给目标的代理对象
我们可以用这种方式图形化地表示这些实体之间的关系:
图 2.6 – 代理对象暴露与目标相同的 API
这种模式的关键因素是代理的行为和暴露的 API 与目标相同,这样客户端就不知道或不需要知道它正在处理的是代理而不是目标对象。那么,我们为什么要这样做呢?有很多很好的理由,例如以下:
-
你需要保持原始未修改的 API,但与此同时:
-
需要处理客户端的输入或输出
-
需要拦截每个 API 调用以添加内部功能,例如维护操作、性能改进、错误检查和验证
-
目标是一个昂贵的资源,因此代理可以实现逻辑来利用它们的操作(例如,缓存)
-
-
你需要更改客户端或目标,但不能修改 API
-
你需要保持向后兼容性
你可能会遇到更多理由,但希望到现在你能够看到这如何有用。作为一个模式,这个模板可以在多个级别上实现,从简单的对象代理到完整的应用程序或服务器。在执行系统的部分升级时,这相当常见。在较低级别上,JavaScript 甚至原生包含一个用于代理对象的构造函数,Vue 3 使用它来创建响应性。
在第一章,Vue 3 框架中,我们回顾了使用ref()进行响应式的选项,但这个 Vue 的新版本还包括另一个用于复杂结构的替代方案,称为reactive()。第一个使用 pub/sub 方法(观察者模式!),但后者使用原生代理处理程序(这个模式!)。让我们看看这个原生实现可能如何与一个简单的部分实现一起工作。
在这个简单的例子中,我们将创建一个具有反应性属性的自动将摄氏度转换为华氏度并反向转换的Proxy对象:
./chapter 2/proxy-1.js
let temperature={celsius:0,fahrenheit: 32}, //1
handler={ //2
set(target, key, value){ //3
target[key]=value; //4
switch(key){
case "celsius":
target.fahrenheit=calculateFahrenheit(value); //5
break;
case "fahrenheit":
target.celsius=calculateCelsius(value);
}
},
get(target, key){
return target[key]; //6
}
},
degrees=new Proxy(temperature, handler) //7
// Auxiliar functions
function calculateCelsius(fahrenheit){
return (fahrenheit - 32) / 1.8
}
function calculateFahrenheit(celsius){
return (celsius * 1.8) + 32
}
degrees.celsius=25 //8
console.log(degrees)
// Prints in the console:
// {celsius:25, fahrenheit:77} //9
让我们逐行审查代码,看看它是如何工作的:
-
在这一行,我们声明了
temperature对象,它将成为我们要代理的目标。我们用相等的转换值初始化其两个属性。 -
我们声明一个
handler对象,它将成为我们的温度对象代理。 -
代理处理程序中的
set函数接收三个参数:目标对象、引用的键以及尝试分配的值。请注意,我说“尝试”,因为操作已被代理拦截。 -
在这一行,我们按照预期将赋值操作应用于对象属性。在这里,我们可能执行其他转换或逻辑,例如验证或引发事件(再次是观察者模式!)。
-
注意我们如何使用 switch 来过滤我们感兴趣的属性名。当键是
celsius时,我们计算并分配华氏值。当我们收到fahrenheit度数的赋值时,情况相反。这就是响应性发挥作用的地方。 -
对于
get函数,至少在这个例子中,我们只是返回请求的值。按照这种方式实现,它将等同于跳过getter函数。然而,它在这里作为一个例子,我们可以操作和转换要返回的值,因为这个操作也被拦截了。 -
最后,在第 7 行,我们使用处理程序将
degrees对象声明为temperature的代理。 -
在这一行,我们通过将摄氏度值赋给
degrees对象的成员来测试响应性,就像我们通常对任何其他对象所做的那样。 -
当我们将
degrees对象打印到控制台时,我们注意到fahrenheit属性已被自动更新。
这是一个相当有限且简单的例子,说明了原生的Proxy()构造函数是如何工作并应用该模式的。Vue 3 使用更复杂的方法来实现响应性和跟踪依赖,这涉及到代理和观察者模式。然而,这让我们对当我们亲眼看到 HTML 实时更新时幕后发生的方法有了很好的了解。
客户端和目标之间代理的概念也与下两个模式相关:装饰器模式和外观模式,因为它们也是一种代理实现。区分的关键因素是代理保留了与原始目标对象相同的 API。
装饰器模式
这种模式乍一看可能非常类似于代理模式,确实如此,但它增加了一些独特的特性,使其与众不同。它确实与代理模式有相同的移动部件,这意味着存在一个客户端、一个目标以及一个在目标之间实现相同接口的装饰器(是的,就像在代理模式中一样)。然而,在代理模式中,拦截的 API 调用主要处理数据和内部维护(“家务”),而装饰器则增强了原始对象的功能以执行更多操作。这是将它们区分开来的决定性因素。
在代理示例中,注意额外的功能是如何作为一个内部反应性来保持每个刻度中的度数同步的。当你改变一个时,它会内部自动更新另一个。在装饰器模式中,代理对象在执行目标对象的 API 调用之前、期间或之后执行额外的操作。就像在代理模式中一样,所有这些对客户端对象都是透明的。
例如,在之前的代码基础上,假设现在我们想要在保持相同功能的同时记录对某个目标 API 的每次调用。从图形上看,它将看起来像这样:
图 2.7 – 一个增强目标以添加日志功能的装饰器示例
在这里,最初只是一个简单的代理,现在仅仅通过执行一个谦逊的日志调用,它已经变成了一个装饰器。在代码中,我们只需要在set()方法结束前添加这一行(假设还有一个名为getTimeStamp()的函数):
console.log(getTimeStamp());
当然,这只是一个简单的例子,只是为了说明问题。在现实世界中,装饰器非常有用,可以在不重写逻辑或代码的很大一部分的情况下为你的应用程序添加功能。在此基础上,装饰器可以是可堆叠的或可链式的,这意味着如果需要,你可以创建“装饰器的装饰器”,这样每个装饰器将代表添加功能的一个步骤,同时保持目标对象的相同 API。就这样,我们开始步入中间件模式的边界,但在这本书中我们不会涉及它。无论如何,那个其他模式背后的想法是创建具有指定 API 的中间件函数层,每个函数执行一个动作,但不同之处在于任何步骤都可以决定终止操作,因此目标可能被调用也可能不被调用。但这又是另一个故事...让我们回到装饰器。
在这本书的之前部分,我们提到 Vue 3 组件没有像通过扩展彼此实现的纯 JavaScript 类那样具有继承。相反,我们可以使用装饰器模式在组件上添加功能或改变视觉外观。现在让我们看看一个简短的例子,因为我们将在第四章中详细讨论组件和 UI 设计,使用组件的用户界面组合。
假设我们有一个最简单的组件,它显示一个谦逊的h1标签,该标签接收以下作为输入:
./chapter 2/decorator-1.vue
<script setup>
const $props=defineProps(['label']) //1
</script>
<template>
<h1>{{$props.label}}</h1> //2
</template>
<style scoped></style>
在这个简单的组件中,我们在第//1行声明了一个名为label的单个输入。现在不用担心语法,因为我们将在*第四章**,使用组件的用户界面组合中详细看到它。在第//2行,我们像预期的那样在h1标签内直接插值值。
因此,为了为这个组件创建一个装饰器,我们需要应用以下简单的规则:
-
它必须代表组件(对象)执行操作
-
它必须遵守相同的 API(输入、输出、函数调用等)
-
它必须在目标 API 的执行之前、之后或期间增强功能或视觉表示
考虑到这一点,我们可以创建一个装饰器组件,它拦截标签属性,稍作修改,并也修改目标组件的视觉外观:
./chapter 2/decorator-2.vue
<script setup>
import HeaderH1 from "./decorator-1.vue"
const $props=defineProps(['label']) //1
</script>
<template>
<div style="color: purple !important;"> //2
<HeaderH1 :title="$props.label+'!!!'"> //3
</HeaderH1>
</div>
</template>
在此代码中,在行//1中,您可以看到我们保持了与目标组件(我们在上一行导入的)相同的接口,然后在行//2中,我们修改(增强)了color属性,在行//3中,我们通过添加三个感叹号来修改传递给目标组件的数据。通过这些简单的任务,我们保持了构建装饰器模式扩展到 Vue 3 组件的条件。这并不坏。
装饰器非常有用,但还有一个类似于代理的、也非常常见且实用的模式:界面(facade)模式。
界面模式
到目前为止,您可能已经看到了这些模式中的渐进模式。我们从代理开始,代表另一个对象或实体执行操作,通过使用装饰器增强了它,同时保持了相同的 API,现在轮到界面模式了。它的作用除了代理和装饰器的功能外,还要简化 API 并隐藏其背后的巨大复杂性。因此,界面(facade)位于客户端和目标之间,但现在目标是高度复杂的,可能是一个对象,甚至是系统或多个子系统。这种模式也用于更改对象的 API 或限制对客户端的暴露。我们可以将交互想象如下:
图 2.8 – 简化与复杂 API 或系统交互的界面对象
如您所见,界面(facade)的主要目的是提供一个更简单的方法来处理复杂的交互或 API。在我们的示例中,我们将多次使用这个模式,以使用更友好的方法简化浏览器中的原生实现。我们将使用库来封装 IndexedDB 的使用,并在第八章“使用 Web Workers 进行多线程”中创建与 Web Workers 的简化通信。
不言而喻,您之前一定见过这种模式的应用,因为它是现代技术的基础概念之一。在简单的界面(API)背后隐藏复杂性无处不在,并且是 Web 开发的重要组成部分。毕竟,整个互联网极其复杂,有成千上万的移动部件,构成网页的技术几乎像是魔法。没有这种模式,我们仍然会使用零和一进行编程。
在实践中,你会在自己的应用中添加简化层来分解复杂性。实现这一目标的一种方法就是使用提供简化界面的第三方库。在接下来的章节中,我们将使用其中的一些,例如以下这些:
-
Axios:用于处理与服务器所有异步 JavaScript 和 XML(AJAX)通信
-
DexieDB:用于处理到 IndexedDB(浏览器本地数据库)的 API
-
Mitt:用于创建事件管道(我们在观察者模式中提到过)
-
Vue 3:用于创建惊人的 UI
通常,大多数 Web 技术的本地实现都有门面库,这些库经过良好的实战测试。开发者非常擅长简化这些库,并通过开源运动与其他人共享代码。然而,当使用他人的模块时,请确保它们是“安全”的。不要重复造轮子,也不要重复自己。但现在,是时候继续到我们列表中的下一个模式了。
回调模式
回调模式易于理解。当需要在同步或异步操作完成后执行操作时适用。为此,函数调用包括一个参数,该参数是在操作完成后要执行的功能。话虽如此,我们需要区分以下两种代码流类型:
-
同步操作按顺序依次执行。这是基本的代码流,从上到下。
-
异步操作一旦被调用,就会在正常流程之外执行。它们的长度不确定,以及它们的成功或失败。
对于异步情况,回调模式特别有用。例如,考虑一个网络调用。一旦调用,我们不知道从服务器获取答案需要多长时间,也不知道它是否会成功、失败或抛出错误。如果没有异步操作,我们的应用将会冻结,等待直到有结果出现。这不会是一个好的用户体验,尽管从计算上是正确的。
JavaScript 中的一个重要特性是,由于它是单线程的,异步函数不会阻塞主线程,允许执行继续。这是很重要的,因为浏览器的渲染函数是在同一个线程上运行的。然而,这并不是免费的,因为它们确实消耗资源,但它们不会冻结 UI,至少在理论上是如此。在实践中,这将取决于许多因素,这些因素受到浏览器环境和硬件的严重影响。不过,我们还是坚持理论。
让我们来看一个同步回调函数的例子,并将其转换为异步。示例函数非常简单:我们将使用回调模式计算给定数字的斐波那契值。但首先,让我们回顾一下计算公式:
F(0)=0
F(1)=1
F(n)=F(n-1)+F(n-2), with n>=2
因此,这里有一个 JavaScript 函数,它应用公式并接收一个回调来返回值。注意,这个函数是同步的:
./chapter 2/callback-1.js - 同步斐波那契
function FibonacciSync(n, callback){
if(n<2){
callback(n)
} else{
let pre_1=0,pre_2=1,value;
for(let i=1; i<n; i++){
value=pre_1+pre_2;
pre_1=pre_2;
pre_2=value;
}
callback(value)
}
}
注意看,我们不是用return返回值,而是将值作为参数传递给callback函数。什么时候使用这种做法是有用的呢?考虑以下简单的例子:
FibonacciSync(8, console.log);
// Will print 21 to the console
FibonacciSync(8, alert)
// Will show a modal with the number 21
只需替换回调函数,我们就可以显著改变结果的呈现方式。然而,这个示例函数有一个影响用户体验的基本缺陷。由于它是同步的,计算时间与传递的参数成正比:n越大,所需时间越长。使用足够大的数字,我们很容易挂起浏览器,但在那之前,我们就可以冻结界面。你可以通过以下片段测试执行是否是同步的:
console.log("Before")
FibonacciSync(9, console.log)
console.log("After")
// Will output
// Before
// 34
// After
要将这个简单的函数转换成异步函数,你只需将逻辑封装在setImmediate调用中即可。这将使执行脱离正常的工作流程。新的函数现在看起来是这样的:
function FibonacciAsync(n, callback){
setImmediate(()=>{
if (n<2){
callback(n)
} else{
let pre_1=0,pre_2=1,value;
for(let i=1; i<n; i++){
value=pre_1+pre_2;
pre_1=pre_2;
pre_2=value;
}
callback(value);
}
})
}
如您所见,我们使用箭头函数来封装代码,没有任何修改。现在,看看我们用这个函数执行与之前相同的片段时的区别:
console.log("Before")
FibonacciAsync(9, console.log)
console.log("After")
// Will output
// Before
// After
// 34
如您所见,输出片段在34之前输出了After。这是因为我们的异步操作已经按照预期从正常流程中移除。在调用异步函数时,执行不会等待结果,而是继续执行下一个指令。有时这可能会让人困惑,但它非常强大且有用。然而,这种模式并没有规定如何处理错误或失败的操作,或者如何链式或顺序地运行多个调用。处理这些情况有不同的方法,但它们不是模式的一部分。还有另一种处理异步操作的方法,它提供了更多的灵活性和控制:承诺(promises)。我们将在下一节看到这一点,在大多数情况下,你可以互换使用这两种模式。我说“在大多数情况下”,并不是所有情况!
承诺模式
promises模式主要是为了处理异步操作。就像回调一样,承诺函数的调用会使执行脱离正常流程,但它返回一个特殊对象,称为Promise。这个对象提供了一个简单的 API,包括三个方法:then、catch和finally:
-
then方法接收两个回调函数,传统上称为resolve和reject。在异步代码中,它们用于返回成功值(resolve)或失败或负值(reject)。 -
catch方法接收一个error参数,当过程抛出error并中断执行时被触发。 -
finally方法在两种情况下都会执行,并接收一个回调函数。
当一个 Promise 正在运行时,它处于一个不确定状态,直到它被解决或拒绝。Promise 在这个状态下等待的时间没有时间限制,这使得它在处理长时间操作(如网络调用和进程间通信(IPC))时特别有用。
让我们看看如何使用 Promise 实现之前的斐波那契数列示例:
function FibonacciPromise(n) {
return new Promise((resolve, reject) => { //1
if (n < 0) {
reject() //2
} else {
if (n < 2) {
resolve(n) //3
} else {
let pre_1 = 1, pre_2 = 1, value;
for (let i = 2; i < n; i++) {
value = pre_1 + pre_2;
pre_1 = pre_2;
pre_2 = value;
}
resolve(value);
}
}
})
}
初看之下,很容易看出实现方式略有变化。我们从第//1行开始,立即返回一个new Promise()对象。这个构造函数接收一个回调函数,该函数反过来会接收两个名为resolve()和reject()的回调。我们需要在我们的逻辑中使用这些回调,以便在成功时返回值(resolve)或失败时返回值(reject)。注意,我们不需要将我们的代码包裹在setImmediate函数中,因为 Promise 本质上就是异步的。我们现在检查负数,并在这种情况下拒绝操作(第//2行)。我们做的另一个改变是在第//3行和第//4行用resolve()替换了callback()调用。
调用现在也发生了变化:
console.log("Before")
FibonacciPromise(9).then(
value=>console.log(value),
()=>{console.log("Undefined for negative numbers!")}
);
console.log("After")
// Will output:
// Before
// After
// 34
如您所见,我们将then方法链接到调用,并传递给它成功和失败的两个函数(在我们的代码中是resolve和reject)。就像之前一样,我们得到了相同的结果。现在,这可能会显得更冗长(确实是),但好处远远超过了额外的输入。Promise 是可链式的,这意味着对于成功的操作,你可以返回一个新的 Promise,从而实现顺序操作。以下是一个例子:
MyFunction()
.then(()=>{ return new Promise(...)}, ()=>{...})
.then(()=>{ return new Promise(...)}, ()=>{...})
.then(()=>{ return new Promise(...)}, ()=>{...})
.then(()=>{ return new Promise(...)}, ()=>{...})
.catch(err=>{...})
Promise构造函数还公开了其他方法,例如.all,但我将向您推荐查阅文档以深入了解可能性和语法(developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)。尽管如此,仍然相当冗长。幸运的是,JavaScript 为我们提供了一个简化的语法来处理 Promise,即async/await,我们可以将其视为一种更“传统”的编码方式。这仅适用于 Promise 函数的调用,并且只能在函数中使用。
为了举例说明,让我们假设我们有三个返回 Promise 的函数,分别命名为MyFuncA、MyFuncB和MyFuncC(是的,我知道,这些名字不是很好)。每个函数在成功的情况下返回一个单个值(这是一个条件)。然后,这些函数在MyProcessFunction中使用新的语法。以下是声明:
async function myProcessFunction() { //1
try { //2
let a = await MyFuncA(), //3
b = await MyFuncB(),
c = await MyFuncC()
console.log(a + b + c) //4
} catch {
console.log("Error")
}
}
// Invoke the function normally
MyProcessFunction() //5
我们首先使用 async 关键字声明我们的函数(行 //1)。这向解释器表明我们将在函数内部使用 await 语法。一个条件是必须将代码包裹在 try...catch 块中。然后,我们可以在每个承诺函数调用的调用前使用 await 关键字,就像行 //3 一样。到行 //4 时,我们可以确信每个变量都已接收到值。当然,这种方法更容易遵循和阅读。
让我们调查以下行的等价性:
let a=await MyFuncA()
这将与thenable(使用 .then)语法相匹配:
let a;
MyFuncA()
.then(result=>{ a=result; })
然而,这种最后一种语法的缺点是我们需要确保所有变量 a、b 和 c 都有值,我们才能运行行 //4 的 console.log(a+b+c),这意味着需要像这样链式调用:
let a,b,c;
MyFuncA()
.then(result=>{ a=result; return MyFuncB()})
.then(result=>{ b=result; return MyFuncC()})
.then(result=>{ c=result; console.log(a+b+c)})
这种格式更难遵循,当然也更冗长。在这些情况下,async/await 语法更受欢迎。
使用承诺(promises)来封装长时间或不确定的操作,以及与其他我们已看到的模式(如外观模式、装饰器模式等)集成是非常有用的。这是一个重要的模式,我们需要牢记,我们将在我们的应用程序中广泛使用它。
摘要
在本章中,我们看到了软件开发的原则和重要的设计模式,以及使用纯 JavaScript 的示例,在适当的时候,也暗示了使用 Vue 3 的实现。这些模式在第一次看到时可能很难理解,但我们将使用它们,并在本书的其余部分回顾它们,以便本章可以作为参考。这将让您更好地了解根据您应用程序的需求何时以及如何应用不同的模式。
在下一章中,我们将从头开始实现一个项目,并为本书其余部分将要构建的应用程序奠定基础。随着我们的前进,我们将参考这些模式来帮助您巩固它们的应用。
复习问题
-
原则和模式之间的区别是什么?
-
单例模式为什么如此重要?
-
你如何管理依赖关系?
-
哪些模式使得反应性成为可能?
-
模式是否交织在一起?为什么?你能给出一个例子吗?
-
异步编程是什么,为什么它如此重要?
-
你能想到哪些适用于承诺函数的使用案例吗?
第三章:设置工作项目
在前几章中,我们为使用 Vue 3 框架 设计 JavaScript Web 应用程序奠定了理论基础。然而,到目前为止,我们还没有真正进入一个实际项目。这正是本章的内容。我们将使用 Vue 3 伴随的新工具集从头开始创建一个项目,并准备一个我们将在其他项目中使用的模板。按照惯例,这个 Web 应用的初始项目是构建一个 待办事项列表(相当于 Hello World)。随着我们对每个新概念的介绍,我们将过度设计应用程序,使其变得更有用,或者至少更吸引人。
我们在这里将学习的一些实用技能如下:
-
设置你的工作环境和 集成开发 环境(IDE)
-
使用新的命令行工具和新的 Vite 打包器来构建我们的应用程序
-
修改基本模板和文件夹结构以适应 最佳实践 和高级架构 设计模式
-
将现成的 CSS 框架集成到我们的应用程序中
-
配置 Vite 打包器以满足我们的需求
与前几章不同,这一章将主要侧重于实践,并且会对生态系统中每个元素的官方文档进行参考,因为这些内容会不时发生变化。你不需要记住这些步骤,因为从头开始启动项目对于大型项目来说并不常见,而且构建这些项目的工具也在不断进化。让我们开始吧。
技术要求
要遵循本章中的实际步骤,你需要以下内容:
-
一台运行 Windows、Linux 或 macOS 且具有 64 位架构的计算机。我将使用 Ubuntu 22.04,但这些工具是跨平台的,步骤可以在不同的操作系统之间转换(如果有不同之处,我会指出)。
-
Node.js 16.16.0 LTS 以及已安装的 npm(节点包管理器)。你可以在官方文档中找到安装 Node.js 的步骤,网址为
nodejs.org/。构建工具在 Node.js 上运行,所以没有这个,你无法走得很远。Node.js 是一个适用于在服务器和浏览器“外部”运行的 JavaScript 版本,这使得它非常方便且强大。今天的大多数 Web 开发打包器都以某种方式使用 Node.js,如果不是至少为了它提供的极大便利性。 -
一个
Volar插件。官方网站是code.visualstudio.com/,在这本书中,我们将使用这个编辑器作为推荐的开发环境(IDE)来与 Vue 和 Vite 一起工作。 -
Sublime Text(免费试用/付费):这是另一个流行的选择,尤其是在 macOS 用户中。官方网站是
www.sublimetext.com/。 -
Jetbrains WebStorm(免费试用,付费):官方网站是
www.jetbrains.com/webstorm/。 -
Komodo IDE(免费):官方网站是
www.activestate.com/products/komodo-ide/。 -
NetBeans IDE(免费):官方网站是
netbeans.apache.org/。 -
控制台或终端模拟器。Linux 和 macOS 用户对此概念最为熟悉。Windows 用户可以使用命令提示符,某些 IDE 的集成终端,或者从 Microsoft Store 安装Windows Terminal.* 一个现代的网页浏览器,无论是基于 Chromium 引擎(Google Chrome、Microsoft Edge、Opera、Brave、Vivaldi 等)还是 Mozilla Firefox。
安装好这些工具后,我们就可以开始跟随示例和基本项目了。然而,我建议您也安装Git,用于代码版本控制。我们将在本书的第九章 测试和源代码控制中使用它。在现代开发中,很难想象在没有一些工具来跟踪代码更改和版本控制的情况下进行项目工作。Git 已成为行业标准。您可以通过访问官方网站上的文档进行安装:git-scm.com/。
本章的代码文件可以在 GitHub 上找到:github.com/PacktPublishing/Vue.js-3-Design-Patterns-and-Best-Practices/tree/main/Chapter03。
查看以下视频,了解代码的实际应用:packt.link/CmuO9
现在,随着我们的工具准备就绪,我们就可以开始我们的第一个 Vue 3 项目了。
项目设置和工具
我们将使用Vite作为打包器,直接从命令行创建一个新的项目。在您将放置项目的目录中打开一个终端窗口,并按照以下步骤操作:
- 输入以下命令:
$ npm create vite@latest
-
如果出现提示安装附加包,请输入
Y(是)。 -
接下来,您将被提示按照以下顺序输入项目信息:
-
.) 作为名称。 -
chapter-3(或您选择的任何名称)。如果已输入或接受了一个项目名称或接受默认名称,则此选项可能不会显示。如果您输入点(.)作为创建项目的名称,那么此选项将是强制性的。 -
使用箭头键选择
vue并按Enter键。 -
选择版本:就像之前一样,使用箭头键选择 JavaScript(或 TypeScript,但本书我们将使用纯 JavaScript)。
-
接下来,您将看到助手如何根据您的选择下载额外内容并搭建项目。它将创建一个包含多个文件的目录结构。然而,如果我们打算运行项目,我们很快就会发现问题在于它根本无法工作。这是因为搭建过程并没有安装依赖项,只是提供了一个骨架。因此,我们还需要再进行一个步骤,那就是使用npm安装依赖项。在终端中,输入以下命令并按Enter(如果您是在当前目录下安装的;如果不是,首先进入刚刚创建的目录):
$ npm install
包管理器将下载并安装我们项目的依赖项,并将它们放置在一个名为node_modules的新目录中。正如您所猜测的,我们使用 Vite 的Vue开发环境是一个Node.js项目。
依赖项就绪后,现在就是运行项目并查看搭建工具为我们准备了什么的时候了。在终端中,输入以下命令:
$ npm run dev
接下来发生的事情可能相当快。Vite 将解析您的项目文件,并在您的机器上启动一个开发服务器,您可以在浏览器中使用这个网址。您在终端中会看到如下内容:
图 3.1 - 使用 Vite 运行开发服务器的结果
这里最重要的信息是localhost和您的项目网站正在提供服务的端口。显示的毫秒数只是为了让您知道 Vite 启动的速度有多快(如果您问我,这就是炫耀的权利)。接下来,为了查看我们到目前为止的工作结果,请在您的网络浏览器中打开本地地址,您应该会看到一个类似于以下屏幕的网站:
图 3.2:浏览器中的基本 Vite + Vue 项目
这个网站本身已经完全可用,尽管不是非常高效。为了测试 Vue 3 是否工作正常,点击屏幕中间的按钮,您会看到每次点击计数器都会增加。这就是响应性的体现!此外,Vite 为我们提供了一个带有实时更新和热模块替换(HMR)的开发服务器,这意味着只要我们在代码中进行更改并保存文件,网站就会自动更新。在实践中,当开发用户界面时,通常会在浏览器中保持这个自更新网站打开以预览我们的工作,在某些情况下,甚至同时打开几个浏览器。非常方便!
我们已经在这段旅程中取得了进步,但距离终点还远。搭建的网站不过是一个起点。我们将对其进行修改,以更好地满足我们的需求,并在本章的剩余部分创建一个简单的待办事项应用。
在下一节中,我们将更详细地查看我们起始项目的结构和组织。
文件夹结构和修改
在第一章《Vue 3 框架》中,我们提到框架为你的应用程序规定了一些结构。Vue 3 并不是例外,但与其他框架相比,目录结构中使用的约定是最小的。如果你在文件资源管理器中打开你安装项目的目录(无论是从你的操作系统还是在你的 IDE 中),你会找到一个类似这样的结构:
图 3.3:Visual Code 中的项目结构
.vscode文件夹是由 IDE 创建的,node_modules是由npm创建的,用于分配依赖项。我们将忽略它们,因为我们不需要担心或处理它们。从顶部开始,让我们回顾每个目录的作用:
public
这个文件夹包含目录结构和文件,这些文件不会被打包器处理,将被直接复制到最终网站中。你可以在这里自由放置自己的静态内容。这就是你将放置你的图片、网络字体、第三方 CSS 库、图标等的地方。一般来说,这里的文件是那些永远不会被你的代码引用的文件,例如manifest.json、favicon.ico、robots.txt等等。
src
我们将在这里放置我们的 JavaScript、动态 CSS、组件等等。如果我们展开这个文件夹,我们会发现脚手架工具已经创建了一个最小结构,如下所示:
-
一个包含 SVG 文件的
assets文件夹。在这个文件夹中,我们可以包含将被代码或打包器处理的文件。你可以直接将它们导入到你的代码中,打包器将负责在服务器上提供它们时正确映射它们。 -
一个
components文件夹,我们将在这里放置我们的.vue扩展。我们可以根据需要在这里创建目录结构。脚手架工具已经被放置在一个HelloWorld.vue组件中供我们使用。 -
一个
App.vue文件。这是我们应用程序的主组件,也是我们层次结构的根组件。按照惯例,我们这样称呼它。 -
一个
main.js文件,这是我们的应用程序的起点。它负责加载初始依赖项、主组件(App.vue)、创建带有所有额外功能(插件、全局指令和组件)的 Vue 3 应用程序,并将应用程序启动和挂载到网页上。 -
一个
styles.css文件,这是一个全局样式表,将应用于我们的整个应用程序。之前的脚手架工具通常将其放置在assets文件夹中,但现在它已经移动到src/根目录,给它一个更突出的位置。当这个文件被导入到main.js文件中时,它将被解析并与我们的 JavaScript 打包。
现在是时候调查项目根目录中的文件了,按照它们出现的顺序:
-
.gitignore是一个控制从 Git 源代码控制中排除内容的文件。我们将在 第九章 中看到 Git,测试和 源代码控制。 -
index.html是主文件,也是我们 Web 应用的起点。打包器将按照文件出现的顺序开始访问和处理其他文件,首先是index.html。你可以根据需要修改它,因为生成的文件相当基础。注意,在body标签的末尾,脚手架工具包含了一个script标签来加载我们的main.js文件。这个文件就是创建我们的 Vue 应用的文件。与其他自动生成此文件并将其注入index.html的打包器不同,Vite 要求你显式地导入它。除了其他优点之外,这让你可以控制 Vue 应用在网页中加载的时间。 -
package-lock.json由npm用于管理node_modules中的依赖项。忽略它。 -
package.json文件非常重要。该文件定义了项目,跟踪你的开发和生产依赖项,并提供了一些便捷的功能,例如通过简单的命令自动化一些任务。目前值得关注的是scripts部分,它定义了命令的简单别名。我们可以通过在命令行中输入npm run <script name>来运行这些命令。脚手架工具已经为我们准备好了三个 Vite 命令:-
npm run dev:这将以开发者模式启动网站,带有本地服务器和实时刷新。 -
npm run build:这将打包我们的代码并将其优化,以创建一个生产就绪版本。 -
npm run preview:这是前两个选项之间的中间点。它将允许你在本地查看构建的生产就绪版本。这听起来可能有些令人困惑,直到你考虑到,在开发期间,你的应用程序访问的地址和资源,以及公共 URL,可能与生产环境中的不同。此选项允许你在本地运行应用程序,但仍然引用和使用那些生产端点和资源。在部署应用程序之前运行“预览”是一个好的做法。
-
-
vite.config.js是一个配置文件,它决定了 Vite 在开发和打包生产版本时的行为。我们将在本章后面看到一些最重要的或常见的选项。
现在我们已经对 Vite 脚手架工具提供的内容有了更清晰的了解,是时候开始构建我们的示例应用了。在我们深入代码之前,还有几件事情需要处理:如何集成第三方样式表和 CSS 框架,以及一些会使我们的生活更轻松的 Vite 配置。
与 CSS 框架的集成
如果我们还记得在第二章“软件设计原则和模式”中讨论的最后三个原则(不要重复自己、保持简洁和为未来编写代码),那么在视觉外观和图形语言方面重新发明轮子通常是不受欢迎的。网络上有不断增长的 CSS 框架和库集合,我们可以轻松地将它们整合到我们的应用程序中。从旧的流行 Bootstrap 到原子设计,再到像 Tailwind 这样的实用类,以及经过图形语言如 Material Design 和拟物主义,选项范围非常广泛。Vue 已经有一些组件库实现了这些库中的一些,你可以在npm仓库中找到它们。使用这些库,你将局限于了解和应用设计师应用的约定,这在某些情况下可能会使你构建用户界面的方式变得固定。这些典型的例子包括使用Vue-material(以及其他)遵守 Google 的 Material Design 规范或整合网络字体和图标字体。不可能讨论每一个,但这里有一些指南和一些示例,说明如何将这些库整合到你的项目中:
-
按照框架或库提供的静态资产的要求结构,将它们放置在
public文件夹中,并尊重所需的树结构。 -
在你的
index.html文件中包含 CSS 框架或库的依赖项,按照它们的说明进行。通常,这意味着在head部分或body标签中导入样式表和 JavaScript 文件。在任何情况下,确保这些文件在我们应用程序加载之前放置(引用我们的main.js文件的script标签)。 -
如果框架或库需要实例化,请在挂载我们的应用程序之前进行。你可以在
index.html中的script标签、main.js或另一个模块中直接这样做。 -
在组件的模板部分通常使用类(以及 JavaScript 函数),就像在纯 HTML 中使用这些库一样。一些框架会在
window对象上创建 JavaScript 全局对象,因此你可以在组件的script部分直接访问它们。如果不是这种情况,考虑使用设计模式如单例、代理或装饰者模式来封装功能,以便在应用程序中使用。
现在我们将这些简单的说明付诸实践,应用到我们的示例项目中。我们将整合一个仅使用 CSS 的框架(这意味着它不使用额外的 JavaScript),以及字体图标来包含基本的图标。在生产构建中,我们应该删除未使用的 CSS 规则。一些 CSS 框架提供了这个功能,例如 Tailwind (tailwindcss.com/)。然而,这个主题超出了本书的范围,但值得在网上进行研究。
The w3.css 框架
网站 w3school.com 提供了一个基于 Google 流行的 Material Design 语言的部分 CSS 框架,用于许多移动应用程序。它提供了许多你可以免费实施的应用程序实用类。你可以在官方网站上了解更多信息:www.w3schools.com/w3css/。
我们将遵循之前提到的指南,所以让我们按照以下步骤进行:
- 从
www.w3schools.com/w3css/w3css_downloads.asp下载w3.css文件,并将其放置在public目录中名为css的新文件夹中。完成后,它应该看起来像这样:
图 3.4 - w3.css 文件的位置
-
通过添加类似这样的
link标签来修改我们项目的根目录下的index.html文件,引用w3.css文件:<link rel="stylesheet" href="/css/w3.css">
通过这次添加,CSS 文件中定义的类现在可以用于我们的组件模板中。为了避免项目脚手架中不欢迎的样式,请记住清除安装程序提供的styles.css文件。如果我们现在使用npm run dev运行开发服务器,我们会看到网站的外观略有变化,因为新的样式表已经成功应用。下一步现在是要添加一个 图标字体。
FontAwesome 真是太棒了
开发者在处理大量图标时节省资源的一种方法是通过使用 字体图标。这些是字体文件,它们显示图标而不是字符。这个概念并不新鲜,但在网络开发中有许多应用。与 CSS 精灵表等其他技术相比,使用字体作为图标有许多好处。其中最相关的一点是,这些图标可以像常规字体一样进行操作,因此我们可以轻松地改变它们的大小、颜色等,并使它们与文本保持协调。这种方法并非全是快乐和幸福,因为现在,主要的权衡是这些图标最多只能显示一种或两种颜色,并且必须从必要性出发相对简单。
FontAwesome 是一个提供 图标字体 以供我们在应用程序中使用(无论是网页还是桌面)的网站。它已经这样做了很多年,并且拥有一些最好的图标集合。我们可以下载并使用其免费层来为我们项目使用。让我们再次遵循指南,在我们的项目中实现它们:
-
从
fontawesome.com/download下载 网页字体。这将下载一个包含所有不同替代方案的相当大的 ZIP 文件。 -
从 ZIP 文件中,将
css/和webfonts/目录原样复制到我们的public/文件夹中。我们在这里的项目中不会使用这个文件夹中的所有内容,所以你可以稍后删除我们不需要的部分。 -
编辑
index.html文件以添加我们将使用的样式表。这些 CSS 文件将自动从/webfonts/文件夹加载图标字体:<link rel="stylesheet"href="/css/fontawesome.min.css" > <link rel="stylesheet" href="/css/solid.min.css"> <link rel="stylesheet" href="/css/brands.min.css">
这就是我们包含 FontAwesome 到项目中的所有需要做的事情。还有其他一些替代方案已经将字体封装到 Vue 组件中,甚至网站还提供了 Vue 实现。然而,就本书的目的而言,我们将使用直接方法。如果我们打开网站的图标部分,我们可以浏览和搜索所有可用的图标。你可以将搜索限制为“solid”和“brands”,因为这是我们项目中包含的。例如,如果你想使用 FontAwesome 显示 Vue 图标,我们可以在模板中包含以下内容:
<i class="fa-brands fa-vuejs"></i>
这些类在任意空元素中实现所有魔法,但出于传统和方便的考虑,我们总是使用i标签。此外,你甚至不需要手动输入它们。一旦你找到了想要使用的图标,网站提供了一种“点击并复制”代码的便捷功能。上一行代码来自这里:
图 3.5 - FontAwesome 图标页面
让我们记住,当只使用少量图标时,包含大量图标库会影响性能。对于生产构建,请确保你只包含你将在应用程序中使用的图标,通过仅使用必要的图标创建图标字体。就本书的目的和开发过程而言,我们可以跳过这一做法。
配备了漂亮的样式表和一些好的图标字体后,我们几乎可以开始编码了。还有一件事要做,那就是在我们的 Vite 配置中包含一些额外的选项。
Vite 配置选项
vite.config.js文件导出 Vite 将用于开发和生产的配置。Vite 旨在适用于许多不同的框架,而不仅仅是 Vue 3,尽管它是 Vue 3 的官方打包器。当我们打开文件时,我们注意到 Vue 是 Vite 的一个插件。内部,Vite 分别使用Rollup.js(www.rollupjs.org/)和esbuild(esbuild.github.io/)进行开发和生产构建。这意味着我们可以向 Vite 传递选项,还可以通过向这两个底层工具传递参数来对一些边缘情况有更精细的控制。此外,你可以为每种处理模式(开发和生产)传递不同的配置,所以我们在这里并不缺乏选项。
我们将在第十章“部署您的应用程序”中看到一些特定的部署配置,但到目前为止,我们将只关注开发部分,并添加一些内容以避免在代码中过多重复输入。
打开vite.config.js文件并添加以下导入:
import path from "path"
是的,路径导入不是 JavaScript,而是 Node.js,我们可以这样做,因为此文件是在 Node.js 上下文中读取和执行的。它永远不会到达浏览器或任何 JavaScript 上下文。
修改导出配置,使其看起来像这样:
export default defineConfig({
plugins: [vue()],
resolve:{
alias:{
"@components":
path.resolve(__dirname, "src", "components")
}
}
})
在这些行中,我们指定了一个名为@components的别名,与项目路径/src/components匹配。这样,当我们导入组件时,我们可以避免编写相对路径或完整路径,只需以这种方式引用组件内部的导入:
import MyComponent from "@components/MyComponent.vue"
为路径设置别名是一个很好的开发者体验特性。在大型项目中,组件的路径可能会相当长,而且代码重组有时会发生,这使得维护又是一个可能的中断点。定义别名可以让我们通过只在一个地方进行更改来获得更多的灵活性(原则:不要重复自己)。
您可以在vitejs.dev/config.找到 Vite 配置文件的完整参考。Vite 在vitejs.dev/plugins/提供了一个官方插件短列表(例如 Vue 插件),但社区也提供了一些插件来覆盖许多场景,请访问github.com/vitejs/awesome-vite#plugins。这些插件可以在需要时安装并导入到我们的配置文件中。
到目前为止,我们已经完成了足够的准备工作,可以继续前进,最终创建我们的简单待办事项应用程序。
待办事项应用程序
我们的示例应用程序将基于基本应用程序的框架文件构建。它将为我们提供一个输入元素来输入我们的待办事项,并将显示待办和已完成的任务列表。这个练习的目的如下:
-
开发具有实时更新的应用程序
-
创建一个组件,使用
scriptsetup语法中的响应式元素 -
应用第三方库的样式和图标字体
当我们完成时,我们将拥有一个简单的网站,其外观应该如下(已添加待办事项作为示例):
图 3.6 - 应用样式后的最终待办事项应用程序结果
为了这个练习的目的,我们将开发整个待办事项应用程序的一个单一组件,然后将其导入我们的main组件(App.vue)。当然,这是故意打破我们在第二章,软件设计原则和设计模式中看到的一些原则。在第四章,组件用户界面组合中,我们将使用这个产品并通过多个组件“使其正确”。
在应用程序中,用户将执行以下操作:
-
输入简短描述并按Enter键或点击加号将其作为任务输入。
-
系统将显示待办和已完成任务分别在不同的列表中,显示每个组中有多少。
-
用户可以点击任何任务来标记它是否已完成或未完成,应用程序将将其移动到相应的组。
了解应用程序的工作方式后,让我们继续编写代码。
App.vue
这是我们的主要组件。在启动应用程序中,我们需要从每个部分中删除内容,并更改为以下内容(我们将在下一节解释每个部分的作用):
<script setup>
import ToDos from "@components/ToDos.vue"
</script>
在script部分,我们只需要导入一个名为ToDos的组件(我们将在下一节创建此文件)。注意我们是如何使用已经定义的别名来指定路径(@components)。我们的主组件不会处理任何其他数据或功能,我们只使用它作为包装器来控制这个应用程序的布局。考虑到这一点,我们的模板现在将看起来像这样:
<template>
<div class="app w3-blue-gray">
<ToDos />
</div>
</template>
我们声明了一个具有私有类(.app)的div元素,我们将在style部分中定义它。我们还应用了我们从W3.css导入的一种样式,为我们的应用程序添加了背景颜色。在我们的div元素内部,我们放置了ToDos组件。注意我们使用的是与script部分导入时相同的名称,并且使用的是 Pascal 大小写。我们可以使用这种表示法,或者 HTML 的 kebab-case 等价表示法(<to-dos />,单词之间用连字符分隔,且为小写)。然而,建议在我们的模板中始终使用 Pascal 大小写,以避免与现有或未来的 HTML 组件发生冲突。这个名称将在最终的 HTML 中转换为 kebab-case。
接下来,我们将定义样式,使用 CSS 的flex布局将组件居中显示在屏幕中央:
<style scoped>
.app {
display: flex;
justify-content: center;
width: 100vw;
min-height: 100vh;
padding: 5rem;
}
</style>
在主组件就位后,现在让我们在/src/components目录中创建我们的ToDos组件,正确命名为ToDos.vue。
ToDos.vue
在这个组件中,我们将放置这个简单应用程序的所有逻辑。我们需要以下响应式变量:
-
一个变量用于捕获输入框中的文本,并创建我们的任务
-
一个数组,我们将在这里存放具有以下字段的任务对象:一个唯一的 ID、一个描述和一个布尔值,用于指示它是否已完成
-
一个过滤函数或计算属性(或属性),用于仅显示已完成的任务
根据前面的要求,让我们用以下代码填充我们的script部分:
import { ref, computed } from "vue" //1
const //2
_todo_text = ref(""),
_todo_list = ref([]),
_pending = computed(() => { //3
return _todo_list.value.filter(item =>
!item.checked)
}),
_done = computed(() => { //4
return _todo_list.value.filter(item =>
item.checked)
})
function clearToDo() {_todo_text.value = ""} //5
function addToDo() { //6
if (_todo_text.value && _todo_text.value !== "") {
_todo_list.value.push({id: new Date().valueOf(),
text: _todo_text.value, checked: false})
clearToDo()
}
}
我们从第//1行开始导入 Vue 的ref和computed构造函数,因为这是我们在这个应用程序中需要从框架中获取的所有内容。在第//2行,我们开始声明两个常量来指向响应式值:_todo_text,它将在输入元素中存储用户的任务描述,以及_todo_list,它将是我们任务(待办项)的数组。在第//3行和第//4行,我们声明了两个名为_pending和_done的computed属性。第一个将包含所有未完成的待办项的响应式数组,第二个将包含所有标记为完成的项。请注意,通过使用computed属性,我们只需要保留一个包含所有项目的数组。computed属性用于根据我们的需求获取列表的视图段。这与,例如,为每个组保留两个数组并将项目在它们之间移动的常见模式相比,是一种常用的模式。
最后,在第//5行,我们有一个辅助函数来重置项目文本的值,而在第//6行,我们有一个简单的函数,它检查描述的值并创建一个任务(待办项)添加到我们的列表中。重要的是要注意,当我们修改_task_list时,所有依赖于它的属性和变量都将自动重新评估。这种情况与computed属性相同。
在我们的组件逻辑中,要实现我们想要的结果,我们只需要这些。现在,是时候用 HTML 创建模板了。为了方便,我们将代码分成几个部分。代码中突出显示的部分标记了与框架和script部分中的代码有绑定或交互的部分:
<div class="todo-container w3-white w3-card-4"> //1
<!-- Simple header --> //2
<div class="w3-container w3-black w3-margin-0
w3-bottombar w3-border-blue">
<h1>
<i class="fa-solid fa-clipboard-list"></i>
To-Do List
</h1>
</div>
我们的组件模板从第//1行开始,通过定义一个带有一些样式的包装元素来定义。然后,在第//2行,我们放置了一个带有样式和图标字体的简单标题。注意我们是如何同时使用W3 CSS 框架的 CSS 类和我们的作用域样式。接下来的代码行将专注于捕获用户输入:
<!-- User input --> //3
<div class="w3-container flex-container w3-light-gray w3-padding">
<input class="w3-input w3-border-0" type="text"
autofocus
v-model="_todo_text"
@keyup.enter="addToDo()"
placeholder="Type here your to-do item...">
<button class="w3-button w3-gray" @click="clearToDo()">
<i class="fa-solid fa-times"></i>
</button>
<button class="w3-button w3-blue" @click="addToDo()">
<i class="fa-solid fa-plus"></i>
</button>
</div>
与用户的交互从第//3行的部分开始,我们在这里定义了一个输入元素,并使用v-model指令附加我们的_todo_text响应式变量。从这时起,用户在我们输入框中输入的任何内容都将是我们代码中变量的值。为了方便起见,我们还通过以下属性捕获了Enter键:
@keyup.enter="addToDo()"
这将触发脚本中的addToDo函数。我们将在输入字段旁边的加号按钮上添加相同的操作,也是在click事件上:
@click="addToDo()"
这为我们提供了两种方式将我们的描述作为任务添加到待办事项列表中,即使用与同一功能相关联的多个事件。以下代码现在专注于显示输入数据:
<!-- List of pending items --> //4
<div class="w3-padding w3-blue">Pending ({{ _pending.length }})
</div>
<div class="w3-padding" v-for="todo in _pending" :key="todo.id">
<label>
<input type="checkbox" v-model="todo.checked">
<span class="w3-margin-left">
{{ todo.text }}
</span>
</label>
</div>
<div class="w3-padding" v-show="_pending.length == 0">No tasks
</div>
<!-- List of completed tasks --> //5
<div class="w3-padding w3-blue">Completed ({{ _done.length }})
</div>
<div class="w3-padding" v-for="todo in _done" :key="todo.id">
<label>
<input type="checkbox" v-model="todo.checked">
<span class="w3-margin-left">
{{ todo.text }}
</span>
</label>
</div>
<div class="w3-padding" v-show="_done.length == 0"> //6
No tasks
</div>
</div>
要显示我们的任务列表,我们有两个几乎相同的代码块,从第//4行和第//5行开始——一个用于待办任务,另一个用于已完成任务。我们只关注第一个代码块(从第//4行开始),因为这两个代码块的行为几乎相同。在第一个div元素中,我们创建了一个小标题,显示_pending数组中的项目数量,通过插值其长度。我们用以下行来完成这个操作:
Pending ({{ _pending.length }})
注意我们如何在双大括号内直接访问数组属性,而不使用.value属性。虽然在我们的 JavaScript 代码中,我们应该写成_pending.value.length,但当我们使用 HTML 中的插值时,Vue 足够智能,能够识别template部分中的响应式变量并直接访问其值。这对于computed属性以及使用ref()创建的响应式变量同样适用。
在下一个div元素中,我们创建了一个带有v-for/:key指令的列表,该指令将遍历我们的_pending数组并为每个项目创建一个元素副本。在每一个元素中,我们现在可以使用在v-for指令中声明的名称todo来引用每个项目。接下来,我们在label元素内包裹一个input复选框和一个span,并将todo.checked属性(布尔值)绑定到输入框上,使用v-model。Vue 将负责根据复选框的状态分配true或false值。当发生这种情况时,它还会触发computed属性的重新计算,我们会看到只需通过勾选/取消勾选一个项目,它就会在组(待办和已完成)之间移动,并更新每个块的总量。我们还有一个span元素来显示任务的文本。
最后,对于列表组为空的情况,我们还有一个div元素,当该列表在第//``6行(_pending.length==0)为空时才会可见。
如前所述,显示“已完成”列表的部分也是以相同的方式工作,应用相同的逻辑。
在这种情况下,我们的作用域样式将会相当小,因为我们只需要一些额外的设置,因为大部分繁重的工作都是使用w3.css库完成的。在我们的style部分中,添加以下内容:
.todo-container {max-width: 100%; min-width: 30rem;}
label {cursor: pointer; display: flex;}
todo-container类限制了我们的组件的最大和最小宽度,我们还修改了label元素,使用flex布局显示其子元素。
要查看应用程序的实际运行情况,保存所有更改,并在终端中使用以下命令启动 Vite 开发服务器:
$ npm run dev
一旦 Vite 准备就绪,就像我们之前做的那样,在网页浏览器中打开地址。如果一切顺利,你应该会看到我们的待办列表按预期工作。如果不这样,请检查存储库中的源代码,以确保你输入的代码与完整示例匹配。
对我们的待办应用进行快速评估
我们刚刚创建的应用程序正在运行,并且比简单的 Hello World 或计数按钮更高级一些。然而,我们还没有应用所有应该或可以应用的最佳实践和模式。这是故意的,作为一个学习练习。有时,为了知道如何正确构建某物,我们首先需要构建它以使其工作。一般来说,所有工程实践都理解,有一个迭代精炼的过程,每次交互都提供学习和成熟。一旦我们构建了第一个原型,就是时候退后一步,真诚地对其进行分析,思考我们如何改进它并做得更好。在这种情况下,以下是我们的分析:
-
在我们的模板中,代码有重复,因为
_pending和_done计算属性基本上是相同的,只是基于变量值的微小差异。 -
我们没有充分利用组件的力量,因为一切都是在单个组件中构建的。
-
我们的组件也在创建我们的模型(待办事项),因此我们的业务逻辑与我们的组件绑定。
-
在输入清理和控制方面,我们做得很少。可以预见,一些代码,甚至是相等的输入,都会破坏我们的应用程序。
-
我们的待办事项列表是易变的。页面刷新将清除我们的列表。
-
我们的任务只容纳两种状态(完成和待办)。如果我们想有第三种状态或更多状态怎么办?例如,进行中、等待或下一个?
-
当前设计不提供编辑或删除已创建任务的方法。
-
我们一次只能管理一个项目列表。
随着我们继续前进,我们将改进我们的应用程序,并应用原则和模式,使其成为一个更具弹性和有用的应用程序。在下一章中,我们将探讨如何以更易于接受的方式使用 Web 组件来组合 Web 应用程序。
摘要
在本章中,我们已经开始使用真实工具创建应用程序,从 IDE 到命令行工具,以构建、预览和构建我们的应用程序。我们还创建了一个简单的待办事项应用程序,并学习了如何将第三方 CSS 库和图标字体集成到我们的应用程序中,并定义了一些一般性指南以纳入其他库。我们还以批判性的态度对待我们的简单应用程序,作为提高其功能性和技能的步骤。在下一章中,我们将探讨如何更好地组织我们的代码并创建组件层次结构以创建我们的用户界面。
复习问题
-
开发一个使用 Vite 的 Vue 3 应用程序有哪些要求?
-
是否可以将第三方库和框架与 Vue 3 集成?
-
将 CSS-only 库集成到 Vue 应用程序中的一些步骤是什么?
-
在单个组件中创建应用程序是一个好主意吗?为什么是或不是?你能想到哪些场景,单组件应用程序是合适的?又或者,有哪些场景它是不合适的?
-
为什么软件开发是一个迭代精炼的过程?
第四章:使用组件进行用户界面组合
在本章中,我们将更深入地探讨如何使用组件来组合用户界面。虽然我们可以像在第三章中我们的初始待办事项列表应用中那样,只用一个组件创建整个网页,但这并不是一个好的实践,除非是简单的应用、现有应用程序功能的部分迁移,或者在某些边缘情况下没有其他选择。组件是 Vue 构建界面的核心。
在本章中,我们将执行以下操作:
-
学习如何使用组件层次结构来组合用户界面
-
学习组件之间相互交互和通信的不同方式
-
探索特殊和自定义组件
-
创建一个应用设计模式的示例插件
-
使用我们的插件和组件组合重新编写我们的待办事项应用
本章将介绍核心和高级概念,并为您提供构建具有可重用组件的稳健 Web 应用程序的工具。特别是,我们将应用从第二章中学习的设计模式,软件设计原则和模式,在代码实现中。
关于样式的说明
为了避免代码示例过长,我们将省略示例中的图标和样式。完整的代码,包括样式和图标,可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Vue.js-3-Design-Patterns-and-Best-Practices。
技术要求
遵循本章的要求与之前在第三章中提到的相同,设置一个 工作项目。
查看以下视频以查看代码的实际应用:packt.link/eqm4l
本章的代码文件可以在 GitHub 上找到:github.com/PacktPublishing/Vue.js-3-Design-Patterns-and-Best-Practices/tree/main/Chapter04
使用组件进行页面组合
要创建用户界面,我们必须有一个起点,无论是粗糙的草图还是复杂的设计。Web 应用程序的图形设计超出了本书的范围,因此我们将假设它已经创建好了。为了将设计转换为组件,我们可以将其视为一个回答以下问题的过程:
-
我们如何使用组件来表示布局和多个元素?
-
这些组件将如何相互通信和关联?
-
将有哪些动态元素进入或离开场景,以及它们将由哪些事件或应用程序状态触发?
-
考虑权衡,我们可以应用哪些设计模式来最好地满足用例?
Vue 3 非常适合创建动态、交互式的界面。这些问题引导我们到一个可重复的实现方法。所以,让我们定义一个具有明确阶段和步骤的通用过程。
步骤 1 – 识别布局和用户界面元素
此步骤回答的问题是:我们如何用组件来表示布局和多个元素?
我们将考虑整个页面,并根据设计考虑哪种布局最合适。我们应该使用列?部分?导航菜单?内容岛屿?是否有对话框或模态窗口?一种简单的方法是将设计图像取出来,并用矩形标记可能代表组件的部分,从最外层到单个交互单元。迭代这个页面的切割,直到你有一个舒适的组件数量。考虑到新的待办事项应用设计,这一步可能看起来是这样的:
图 4.1 – 将设计切割成带有虚线框的组件
一旦我们确定了组件,我们必须提取它们之间的关系,从最顶层的根组件(通常,这将是我们App.vue)创建一个层次结构。由于按上下文或功能分组组件,可能会出现新的组件。这是命名组件的好时机。这个初始架构将随着我们实现设计模式而演变。按照这个例子,层次结构可能看起来像这样:
图 4.2 – 组件层次结构的初步方法
注意到由于对其他组件的分组,出现了一个新的组件ToDoProject.vue。App组件通常处理应用程序的主要布局和层次结构中的起点。现在,随着我们的初始设计就位,是时候进行下一步了。
步骤 2 – 识别关系、数据流、交互和事件
此步骤回答的问题是:这些组件将如何相互沟通和关联?
在这个阶段,我们需要了解用户的交互(使用用例、用户故事或其他)。对于每个组件,我们决定它将保存什么信息(状态),将传递给其子组件的内容,它需要从其父组件获取的内容,以及它将触发哪些事件。在 Vue 中,组件只能垂直地相互关联。兄弟组件大部分情况下会忽略彼此的存在。如果一个兄弟组件需要与另一个组件共享数据,那么这些数据必须由一个可以与双方共享的第三方托管,通常是具有共同可见性的父组件。还有其他解决方案,例如响应式状态管理,我们将在第七章,数据流管理中详细讨论。对于本章,我们将满足基本的关联功能。
记录这些信息有许多方法:在层次结构树中手写的笔记(见 图 4.2),描述性的正式文档,UML 图表(UML 代表 通用建模语言,是软件组件的图标表示),等等。为了简单起见,我们只将树的一个部分以表格格式写下:
| 组件 | 功能 | 状态,输入/输出,事件 | |
|---|---|---|---|
| ToDoProject.vue | 托管待办事项列表并与用户协调交互。此组件将主动修改项目。 | 状态:待办事项列表 | 事件:打开新项目、编辑和删除模态框 |
| ToDoSummary.vue | 显示按状态分类的待办事项汇总。 | 输入:待办事项列表 | 状态:每个项目状态的计数器 |
| ToDoFilter.vue | 收集一个字符串以过滤待办事项列表。 | 输出:一个过滤字符串 | 状态:一个辅助变量 |
| ToDoList.vue | 显示待办事项列表,以及每个项目的信号操作。 | 输入:待办事项列表,一个过滤字符串 | 事件:切换项目状态,编辑和删除项目 |
为了简洁,我省略了将构成用户对话框的组件和交互。我们将在本章后面看到它们,但可以简单地说,ToDoProject.vue 负责使用模态对话框管理交互。
第 3 步 – 识别用户交互元素(输入、对话框、通知等)
此步骤回答的问题是:哪些动态元素将进入或离开场景,以及它们将触发哪些事件或应用程序状态?
在我们的应用程序中,主要的 CRUD 操作(ToDoProject.vue 组件作为对某些事件的响应来控制此交互)由 ToDoProject.vue 组件执行。此过程在本序列图中表示:
图 4.3 – 通过模态框进行用户交互 – 编辑项目
在此图中,ToDoProject 组件与 ToDoList 组件共享待办事项列表。当用户触发 edit 事件时,子组件通过引发此类事件来通知父组件。然后,父组件复制项目并打开一个模态对话框,传递该副本。当对话框被接受时,父组件使用更改修改原始项目。然后,Vue 的响应性反映了子组件中的状态变化。
通常,这些交互帮助我们识别在 第 1 步 中不明显需要的额外组件,例如设计模式的实现……这是下一步。
第 4 步 – 识别设计模式和权衡
此步骤回答的问题是:在考虑权衡的情况下,我们可以应用哪些最佳设计模式来满足用例?
决定使用哪些模式可能是一个非常创造性的过程。没有银弹,多个解决方案可以提供不同的结果。通常需要制作几个原型来测试不同的方法。
在我们的新应用程序中,我们引入了模态对话框的概念来捕获用户输入。当操作需要用户操作或决策才能继续时,会使用模态对话框。用户可以接受或拒绝对话框,并且在做出决定之前不能与应用程序的任何其他部分交互。在这些条件下,一个可能的模式是应用 异步 Promise 模式。
在我们的代码中,我们希望将模态对话框作为一个 promise 打开,这个 promise 按定义将提供给我们一个 resolve()(接受)或 reject()(取消)函数。此外,我们希望能够在多个项目中,以及在我们的应用程序中全局地使用这个解决方案。我们可以创建一个插件来实现这个目的,并使用 依赖注入模式 从任何组件访问模态功能。这些模式将为我们提供所需的解决方案,使我们的模态对话框可重用。
在这个阶段,我们几乎准备好从概念上开始实现组件。然而,为了创建一个更适合且更坚固的应用程序,并实现上述模式,我们应该花点时间来更多地了解 Vue 组件。
深入了解组件
组件是框架的构建块。在 第一章,Vue 3 框架 中,我们看到了如何与组件一起工作,声明响应式变量,以及更多。在本节中,我们将探索更多高级功能和定义。
本地组件和全局组件
当我们开始我们的 Vue 3 应用程序时,我们在 main.js 文件中将主组件 (App.vue) 挂载到一个 HTML 元素上。之后,在各个组件的脚本部分,我们可以通过以下命令导入其他组件以本地使用:
import MyComponent from "./MyComponent.vue"
以这种方式,为了在另一个组件中使用 MyComponent,我们需要在这个组件中再次导入它。如果一个组件在多个组件中连续使用,这种重复操作会打破开发 DRY 原则(参见 第二章,软件设计原则和模式)。另一种选择是将组件声明在 main.js 文件中,我们可以使用 App.component() 方法来实现这种情况:
Main.js
Import { createApp } from "vue"
import App from './App.vue'
Import MyComponent from "./MyComponent.vue"
createApp(App)
.component('MyComponent', MyComponent)
.mount("#app")
component() 方法接收两个参数:一个表示组件 HTML 标签的 String,以及一个包含组件定义的对象(可以是导入的或内联的)。注册后,它将可供我们应用程序中的所有组件使用。然而,使用全局组件有一些缺点:
-
即使从未使用过,组件也将包含在最终的构建中
-
全局注册会模糊组件之间的关系和依赖
-
本地导入的组件可能会发生名称冲突
建议只全局注册那些提供通用功能的组件,并避免那些是工作流程或特定上下文不可或缺部分的组件。
静态、异步和动态导入
到目前为止,我们导入的所有组件都使用 import XYZ from "filename" 语法以静态方式定义。例如 Vite 这样的打包器将它们包含在一个单一的 JavaScript 文件中。这增加了包的大小,并且可能会在我们的应用程序启动时造成延迟,因为浏览器需要下载、解析和执行包及其所有依赖项,然后才能进行任何用户交互。此代码可能包含很少使用或访问的功能。这种做法的明显替代方案是将我们的包文件分割成多个较小的文件,并在需要时加载它们。在这种情况下,我们有两种方法——一种由 Vue 3 提供,另一种由最新的 JavaScript 动态导入语法提供。
Vue 3 提供了一个名为 defineAsyncComponent 的函数。此函数接受一个参数,即返回动态导入的另一个函数。以下是一个示例:
import {defineAsyncComponent} from "vue"
const MyComponent = defineAsyncComponent(
()=>import("MyComponent.vue")
)
使用此函数使其在大多数打包器中安全使用。Vue Router 使用此语法的替代方案,我们将在第五章“单页应用程序”中看到:JavaScript 提供的 import() 动态声明(developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import),其语法非常相似:
const MyComponent = () => import('./MyComponent.vue')
如您所见,此语法更简洁。然而,它只能在定义 Vue Router 路由时使用,因为 Vue 3 和 Vue Router 处理懒加载组件的方式在内部是不同的。最终,两种方法都将主包文件分割成多个较小的文件,这些文件将在需要时自动加载到我们的应用程序中。
然而,defineAsyncComponent 有一些优点。我们还可以传递任何返回解析为组件的 promise 的函数。这允许我们在运行时动态地控制过程。以下是一个示例,其中我们根据输入值的值决定加载一个组件:
const ExampleComponent=defineAsyncComponent(()=>{
return new Promise((resolve, reject)=>{
if(some_input_value_is_true){
import OneComponent from "OneComponent.vue"
resolve(OneComponent)
}else{
import AnotherComponent from
"AnotherComponent.vue"
resolve(AnotherComponent)
}
})
})
defineAsyncComponent 的第三种语法可能是最有用的。我们可以将一个具有属性的对象作为参数传递,这提供了对加载操作的更多控制。它具有以下属性:
-
loader(必填):它必须提供一个返回加载组件的 promise 的函数 -
loadingComponent: 在异步组件加载时显示的组件 -
delay: 在显示loadingComponent之前等待的毫秒数 -
errorComponent: 如果 promise 拒绝或由于任何原因加载失败时显示的组件 -
timeout: 在认为操作失败并显示errorComponent之前的时间(毫秒)
这里是一个使用所有这些属性的示例:
const HeavyComponent = defineAsyncComponent({
loader: ()=> import("./HeavyComponent"),
loadingComponent: SpinnerComponent,
delay: 200,
errorComponent: LoadingError,
timeout: 60000
})
当浏览器从 loader 属性检索组件时,我们显示 SpinnerComponent 来通知用户操作正在进行中。根据 timeout 定义的 1 分钟后,它将自动显示 LoadingError 组件。
采用这种方法,我们的代码优化得更好。现在,让我们学习如何通过事件接收数据和通知其他组件。
属性、事件和 v-model 指令
我们已经看到了使用属性和事件作为在组件与其父组件之间传递数据的基本用法。但通过多种语法,可以实现更强大的定义。属性可以在 script setup 语法中使用 defineProps 和以下任何参数格式定义:
- 作为字符串数组 – 例如:
const $props=defineProps(``)
- 作为对象,其属性用作名称,值是数据类型 – 例如,
const $props=defineProps(``)
作为对象,其属性定义了一个具有类型和默认值的对象 – 例如,
const $props=defineProps({
** name: { type: String, default: “John”},**
** last_name: {type: String, default: “Doe”}**
)
我们需要记住,原始值是通过 value(意味着在子组件内部更改它们的值不会影响父组件中的值)传递给组件的。然而,复杂的数据类型,如对象和数组,作为 引用 传递,因此它们内部键/值的更改将反映在父组件中。
关于复杂类型的说明
当使用默认值定义 Object 或 Array 类型的属性时,默认属性必须是一个返回该对象或数组的函数。否则,对象/数组的引用将由组件的所有实例共享。
事件 是子组件向父组件发出的信号。这是在 script setup 语法中定义组件事件的示例:
const $emit=defineEmits(['eventName'])
与属性不同,emits 只接受字符串声明的数组。事件还可以向接收者传递值。以下是从上述定义中调用的示例:
$emit('eventName', some_value)
如您所见,defineEmits 返回一个函数,该函数接受定义数组中提供的相同名称作为第一个参数。第二个参数 some_value 是可选的。
自定义输入控制器
当属性和事件共同作用时,有一个特殊的应用是创建自定义输入控制器。在之前的示例中,我们使用了 Vue 的 v-model 指令在基本的 HTML 输入元素上捕获它们的值。遵循特殊命名约定的属性和事件允许我们创建接受 v-model 指令的输入组件。让我们看一下以下代码:
父组件模板
<MyComponent v-model="parent_variable"></MyComponent>
现在我们已经在父组件中使用了 MyComponent,让我们看看我们如何创建连接:
MyComponent 脚本设置
const $props=defineProps(['modelValue']),
$emit=defineEmits(['update:modelValue'])
我们使用 Props 的数组定义来简化。请注意,prop 的名称是 modelValue,事件是 update:modelValue。这种语法是预期的。当父组件使用 v-model 分配变量时,值将被复制到 modelValue。当子组件发出 update:modelValue 事件时,父组件变量的值将被更新。这样,您可以创建强大的输入控件。但还有更多——您可以有多个 v-model!
让我们考虑在使用 v-model 时 modelValue 是默认值。Vue 3 引入了一种新的语法来处理这个指令,以便我们可以拥有多个模型。声明非常简单。考虑以下子组件的声明:
子组件的 props 和事件
const
$props=defineProps(['modelValue', 'title']),
$emit=defineEmits(['update:modelValue','update:title'])
在前面的 props 和 emits 定义之后,我们现在可以从父组件中引用它们,如下例所示:
父组件模板
<ChildComponent v-model="varA" v-model:title="varB"></ChildComponent>
如我们所见,我们可以在 v-model:name_of_prop 指令上附加一个修饰符。在 Child 组件中,事件的名称现在必须包含 update: 前缀。
使用 props 和事件允许在父组件和子组件之间直接进行数据流。这意味着如果需要与多个子组件共享数据,它必须在父级进行管理。当父组件需要将数据传递给不是子组件,而是孙组件或其他深层嵌套的组件时,这种限制就会产生一个问题。这就是 依赖注入模式 来拯救的时刻。Vue 通过 Provide 和 Inject 函数自然地实现了这一点,我们将在下一节中更详细地介绍。
使用 Provide 和 Inject 进行依赖注入
当父组件中的数据需要在深层嵌套的子组件中可用时,仅使用 props,我们就必须在组件之间“传递”数据,即使它们不需要或使用这些数据。这个问题被称为 props 钻孔。同样,事件在相反方向上“冒泡”时也会发生这种情况。为了解决这个问题,Vue 提供了名为 Provide 和 Inject 的两个函数来实现依赖注入模式。使用这些函数,父或根组件可以 提供 数据(无论是按值还是按引用,如对象),这些数据可以被 注入 到其层次树中的任何子组件中。直观上,我们可以将这种情况表示如下:
图 4.4 – Provide/Inject 的表示
如您所见,这个过程非常简单,以及实现该模式的语法:
-
在父(根)组件中,我们导入 Vue 的
provide函数并创建一个带有键(名称)和要传递的数据的提供:import {provide} from "vue" provide("provision_key_name", data) -
在接收组件中,我们导入
inject函数并通过键(名称)检索数据:import {} from "vue" const $received_data = (")
我们还可以以下列方式在应用级别提供资源:
const app = createApp({})
app.provide('provision_key_name', data_or_value)
这样,提供的内容可以注入到我们应用程序的任何组件中。值得一提的是,我们还可以提供复杂的数据类型,例如数组、对象和响应式变量。在以下示例中,我们提供了一个包含函数和父方法引用的对象:
在父/根组件中
import {provide} from "vue"
function logMessage(){console.log("Hi")}
const _provision_data={runLog: logMessage}
provide("service_name", _provision_data)
在子组件中
import {inject} from "vue"
const $service = inject("service_name")
$service.runLog()
在这个例子中,我们有效地提供了一个Admin.Users.Individual.Profile,它比user_data更具描述性。命名约定(类似于路径的命名只是一个建议,而不是标准)由团队和开发者来定义。正如本书之前提到的,一旦你决定了一个约定,重要的是在整个源代码中保持一致性。在本章的后面部分,我们将使用这种方法创建一个用于显示模态对话框的插件,但在那之前,我们需要了解一些关于特殊组件和模板的更多概念。
特殊组件
组件的层次结构非常强大,但也有局限性。我们已经看到如何应用依赖注入模式来解决这些问题之一,但还有其他一些情况需要更多的灵活性、可重用性或力量来共享代码或模板,甚至将渲染在层次结构之外的组件移动过来。
插槽,插槽,还有更多的插槽...
通过使用 props,我们的组件可以接收 JavaScript 数据。通过类比推理,我们也可以使用称为插槽的占位符将模板片段(HTML、JSX 等)传递到组件模板的特定部分。就像 props 一样,它们接受几种类型的语法。让我们从最基本的一个开始:默认插槽。
假设我们有一个名为MyMenuBar的组件,它充当顶部菜单的占位符。我们希望父组件以我们使用常见 HTML 标签(如header或div)的方式填充选项,如下所示:
父组件
<MyMenuBar>
<button>Option 1</button>
<button>Option 2</button>
</MyMenuBar>
MyMenuBar 组件
<template>
<div class="...">
<slot></slot>
</div>
</template>
假设我们在MyMenuBar中应用了必要的样式和类,最终的渲染模板可能看起来像这样:
图 4.5 – 使用插槽的菜单栏
应用到的逻辑非常直接。<slot></slot>占位符将在运行时被父组件在子标签内提供的任何内容所替换。在前面的示例中,如果我们检查渲染后的最终 HTML,我们可能会发现如下内容(考虑到我们正在使用W3.css类):
<div class="w3-bar w3-border w3-light-grey">
<button>Option 1</button>
<button>Option 2</button>
</div>
这是用户界面设计中的一个基本概念。现在,如果我们需要多个“插槽”——例如,创建一个布局组件呢?在这里,一个称为命名插槽的替代语法就派上用场了。考虑以下示例:
MyLayout 组件
<div class="layout-wrapper">
<section><slot name="sidebar"></slot></section>
<header><slot name="header"></slot></header>
<main><slot name="content"></slot></main>
</div>
如您所见,我们通过 name 属性 命名了每个槽。在父组件中,我们现在必须使用带有 v-slot 指令的 template 元素来访问每个槽。以下是一个父组件如何使用 MyLayout 的示例:
父组件
<MyLayout>
<template v-slot="sidebar"> ... </template>
<template v-slot="header"> ... </template>
<template v-slot="content"> ... </template>
</MyLayout>
v-slot 指令接收一个参数,匹配槽名称,以下是一些注意事项:
-
如果名称不匹配任何可用的槽,则内容不会被渲染。
-
如果没有提供名称,或者使用了名称
default,那么内容将在默认的无名槽中渲染。 -
如果模板没有提供内容,那么在槽定义内部的默认元素将被显示。默认内容放置在槽标签之间:
<slot>...defaultcontent here...</slot>。
v-slot 指令也有简写符号。我们只需在槽名称前加上一个数字符号(#)。例如,前面父组件的模板可以简化如下:
<template #sidebar> ... </template>
<template #header> ... </template>
<template #content> ... </template>
Vue 3 中的槽非常强大,甚至允许在需要时将属性传递给父组件。语法取决于我们是否使用 默认槽 或 命名槽。例如,考虑以下组件模板定义:
向上传递属性组件
<div>
<slot :data="some_text"></data>
</div>
在这里,槽将一个名为 data 的属性传递给父组件。父组件可以使用以下语法访问它:
接收槽属性的父组件
<PassingPropsUpward v-slot="upwardProp">
{{upwardProp.data}} //Renders the content of some_text
</PassingPropsUpward>
在父组件中,我们使用 v-slot 指令并给槽传递的属性分配一个本地名称——在这种情况下,upwardProp。这个变量将接收一个类似于属性对象的对象,但作用域限于元素。正因为如此,这类槽被称为 命名作用域槽,语法类似。看看这个例子:
<template #header="upwardProp">
{{upwardProp.data}}
</template>
槽还有其他高级用法,涵盖了边缘情况,但在这本书中我们将不涉及这些。相反,我鼓励您在官方文档中进一步研究这个主题:vuejs.org/guide/components/slots.html。
与此主题相关的一个概念我们将在本书的后面章节中看到,即 第七章,数据流管理,它适用于响应式中心状态管理。现在,让我们看看一些有点不寻常的特殊组件。
组合式和混入
在 Vue 2 中,有一个名为 composables 的特殊组件。
组合式是一个使用组合式 API 封装和重用组件之间 状态逻辑 的函数。区分组合式与服务类或其他 业务逻辑 的封装非常重要。组合式的主要目的是共享 用户界面或用户交互逻辑。一般来说,每个组合式都执行以下操作:
-
提供一个返回 响应式 变量的函数。
-
遵循以
use为前缀的camelCase命名约定 – 例如,useStore()、useAdmin()、useWindowsEvents()等。 -
它在其自己的模块中是自包含的。
-
它处理状态逻辑。这意味着它管理随时间持续变化的数据。
组合式的经典例子是将其附加到环境事件(窗口大小调整、鼠标移动、传感器、动画等)上。让我们实现一个简单的组合式组件,它读取文档的垂直滚动:
DocumentScroll.js
import {ref, onMounted, onUnmounted} from "vue" //1
function useDocumentScroll(){
const y=ref(window.scrollY) //2
function update(){y.value=window.scrollY}
onMounted(()=>{
document.addEventListener('scroll', update)}) //3
onUnmounted (()=>{
document.removeEventListener('scroll', update)}) //4
return {y} //5
}
export {useDocumentScroll}; //6
在这个小组合式组件中,我们首先从 Vue 导入组件的生命周期事件和响应式构造函数(//1)。我们的主函数useDocumentScroll包含了我们稍后要共享和导出的全部代码(//6)。在//2中,我们创建一个响应式常量并将其初始化为当前窗口的垂直滚动位置。然后,我们创建一个内部函数update,它更新y的值。我们在//3中将这个函数作为监听器添加到文档滚动事件,然后在//4中移除它(来自第二章的*“清理自己的东西,”*原则)。最后,在//5中,我们返回一个包裹在对象中的响应式常量。然后,在一个组件中,我们这样使用这个组合式组件:
SomeComponent.js – 脚本设置
import {useDocumentScroll} from "./DocumentScroll.js"
const {y}=useDocumentScroll()
...
一旦我们导入了响应式变量,我们就可以像往常一样在我们的代码和模板中使用它。如果我们需要在多个组件中使用这段逻辑,我们只需导入组合式组件(DRY原则)。
最后,vueuse.org/为我们项目提供了一个令人印象深刻的组合式组件集合。它值得一看。
带有“component:is”的动态组件
Vue 3 框架提供了一个名为<component>的特殊组件,其任务是作为一个占位符动态渲染其他组件。它使用一个特殊属性:is,可以接收一个带有组件名称的String,或者一个带有组件定义的变量。它还接受一些基本表达式(一个解析为值的代码行)。以下是一个使用表达式的简单示例:
CoinFlip 组件
<script setup>
import Heads from "./heads.vue"
import Tails from "./tails.vue"
function flipCoin(){return Math.random() > 0.5}
</script>
<template>
<component :is="flipCoin()?Heads:Tails"></component>
</template
当这个组件被渲染时,我们将根据flipCoin()函数的结果看到Heads或Tails组件。
在这一点上,你可能想知道,为什么不使用简单的v-show/v-if?当动态管理组件并且不知道在创建模板时哪些组件可用时,这个组件的力量就显现出来了。我们将在第五章中看到的官方 Vue Router,单页应用,使用这个特殊组件来模拟页面导航。
然而,有一个边缘情况,我们需要注意。虽然大多数模板属性将传递到动态组件,但某些指令(如v-model)在原生输入元素上不起作用。这种情况非常罕见,我们不会详细讨论,但可以在官方文档中找到vuejs.org/api/built-in-special-elements.html#component。
现在我们对组件有了更深入的了解,让我们将新知识应用于两个项目:一个插件和我们的待办事项应用的新版本。
一个现实世界的例子 – 模态插件
我们已经看到了多种在项目内部共享数据和功能的方法。插件是在项目之间共享功能并同时增强系统功能的设计模式。Vue 3 提供了一个非常简单的接口来创建插件并将它们附加到我们的应用实例。任何暴露install()方法或接受相同参数的函数都可以成为插件。插件可以执行以下操作:
-
注册全局组件和指令
-
在应用级别注册可注入的资源
-
为应用创建和附加新的属性或方法
在本节中,我们将创建一个实现模态对话框作为全局组件的插件。我们将使用依赖注入来提供它们作为资源,并利用 Vue 的响应性通过 promises 来管理它们。
设置我们的开发项目
按照第三章中的说明,设置工作项目,以便您有一个起点。在src/目录下,创建一个名为plugins/的新文件夹,并在其中创建一个名为modals/的子文件夹。将插件放置在plugins/文件夹内的单独目录中是一种标准做法。
设计
我们的插件将全局安装一个组件,并保持一个内部响应式状态来跟踪当前模态对话框的状态。它还将提供一个 API,以便将其作为依赖项注入需要打开模态对话框的组件。这种交互可以表示如下:
图 4.6 – 模态插件表示
组件将实现模态元素,我们将通过代码打开对话框。当模态打开时,它将返回一个遵循异步模式的 promise。当用户接受模态时,promise 将解析,并在取消时拒绝。模态的内容将由父组件通过使用插槽提供。
实现
对于这个插件,我们只需要两个文件——一个用于插件的逻辑,另一个用于我们的组件。请在前往src/plugins/modal文件夹中创建index.js和Modal.vue文件。目前,只需使用该节段的脚本设置、模板和样式来搭建组件。我们稍后再回来完成它。有了这些文件,让我们一步一步地开始处理index.js文件:
/src/plugins/modals/index.js
import { reactive } from "vue" //1
import Modal from "./Modal.vue"
const
_current = reactive({}), //2
api = {}, //3
plugin = {
install(App, options) { //4
App.component("Modal", Modal)
App.provide("$modals", api)
}
}
export default plugin
我们从//1开始,从 Vue 导入reactive构造函数和一个我们尚未创建的Modal组件。然后在//2行中创建一个内部状态属性_current,在//3行中创建一个将成为我们的 API 的对象。目前,这些只是占位符。重要的部分在//4行,我们在这里定义了install()函数。这个函数接收两个参数:
-
应用程序实例(
App)。 -
如果在安装过程中传递了选项,则包含选项的对象。
使用应用程序实例,我们将Modal注册为全局组件,并将 API 作为名为$modals的可注入资源在应用程序级别提供。为了在我们的应用程序中使用此插件,我们必须将其导入到main.js中,并使用use方法进行注册。代码如下:
/src/Main.js
import { createApp } from 'vue'
import App from './App.vue'
import Modals from "./plugins/modals"
createApp(App).use(Modals).mount('#app')
如您所见,创建和使用插件相当简单。然而,到目前为止,我们的插件并没有做很多事情。让我们回到我们的插件代码,并完成 API。我们需要的是以下内容:
-
一个
show()方法,它接受一个标识模态对话框实现的名称,并返回一个 promise。然后我们将名称和resolve()和reject()函数的引用保存在我们的响应式状态中。 -
accept()和cancel()方法,分别用于解决和拒绝 promise。 -
一个
active()方法,用于检索当前模态的名称。
按照这些指南,我们可以完成代码,使我们的index.js文件看起来像这样:
/src/plugins/modals/index.js
import { reactive } from "vue"
import Modal from "./Modal.vue"
const
_current = reactive({name:"",resolve:null,reject:null}),
api = {
active() {return _current.name;},
show(name) {
_current.name = name;
return new Promise(
(resolve = () => { }, reject = () => { }) => {
_current.resolve = resolve;
_current.reject = reject;
})
},
accept() {_current.resolve();_current.name = "" },
cancel() {_current.reject();_current.name = "" }
},
plugin = {...} // Omitted for brevity
export default plugin;
我们使用reactive变量来保持内部状态,并且只通过我们的 API 进行访问。一般来说,这对任何 API 来说都是一个很好的设计。现在,是时候在我们的Modal.vue组件中施展魔法,以完成工作流程了。为了简洁,我省略了类和样式,但完整的代码可以在本书的 GitHub 仓库github.com/PacktPublishing/Vue.js-3-Design-Patterns-and-Best-Practices中找到。
我们的模式组件需要执行以下操作:
-
使用半透明元素覆盖整个可查看区域,以阻止与应用程序其他部分的交互
-
提供要显示的对话框:
-
一个属性用于注册组件的名称,由父组件提供。
-
一个标题用于显示标题。标题也将是一个属性。
-
一个区域供父组件填充自定义内容。
-
包含带有接受和取消按钮的页脚。
-
一个在组件应该出现时触发的响应式属性。
-
在我们的定义到位后,让我们开始处理模板:
/src/plugins/modals/Modal.vue
<template>
<div class="viewport-wrapper" v-if="_show"> //1
<div class="dialog-wrapper">
<header>{{$props.title}}</header> //2
<main><slot></slot></main> //3
<footer>
<button @click="closeModal(true)">Accept</button> //4
<button @click="closeModal(false)">Cancel</button>
</footer>
</div>
</div>
</template>
在第 //1 行,响应式变量 _show 控制对话框的可见性。我们在第 //2 行显示 title prop,并在第 //3 行预留一个槽位。第 //4 行的按钮将在点击事件中关闭模态,每个按钮都有一个代表性的布尔值。
现在,是时候编写组件的逻辑了。在我们的脚本中,我们需要做以下事情:
-
定义两个 props:
title(用于显示)和name(用于识别) -
注入
$modals资源,以便我们可以与 API 交互并执行以下操作:-
检查模态的名称是否与当前组件匹配(这“打开”了模态对话框)
-
通过解决或拒绝承诺来关闭模态
-
按照这些指示,我们可以完成我们的 script setup:
<script setup>
import { inject, computed } from "vue" //1
const
$props = defineProps({ //2
name: { type: String, default: "" },
title: { type: String, default: "Modal dialog" }
}),
$modals = inject("$modals"), //3
_show = computed(() => { //4
return $modals.active() == $props.name
})
function closeModal(accept = false) {
accept?$modals.accept():$modals.cancel() //5
}
</script>
我们从第 //1 行开始,导入 inject 和 computed 函数。在第 //2 行,我们创建具有合理默认值的 props。在第 //3 行,我们注入 $modals 资源(依赖项),我们将在第 //4 行的 computed 属性中使用它来检索当前活动模态并将其与组件进行比较。最后,在第 //5 行,根据按钮的点击,我们触发承诺的解决或拒绝。
要在我们的应用中的任何组件中使用此插件,我们必须遵循以下步骤:
-
在
template中,定义一个名为我们在插件中注册的组件(Modal)。注意使用属性为 props:<Modal name="myModal" title="Modal example"> Some important content here </Modal> -
在我们的脚本设置中,使用以下代码注入依赖项:
const $modals = inject("$modals") -
使用以下代码通过给定的名称显示模态组件:
$modals.show("myModal").then(() => { // Modal accepted. }, () => { // Modal cancelled })
使用这个,我们已经完成了我们的第一个 Vue 3 插件。让我们在我们的新待办事项应用中好好利用它。
实现我们的新待办事项应用
在本章的开头,我们看到了我们新待办事项应用的设计,并将其切割成层次组件(见 图 4*.1*)。为了跟随本节的其余部分,你需要从本书的 GitHub 仓库中获取源代码副本(github.com/PacktPublishing/Vue.js-3-Design-Patterns-and-Best-Practices)。随着我们的代码库的增长,不可能详细查看每个实现,因此我们将关注主要更改和特定的代码片段。考虑到这一点,让我们回顾前一个实现中的更改,大致按照文件执行顺序。首先,我们在项目中添加了两个新的目录:
-
/src/plugins,我们放置我们的Modals插件的位置。 -
/src/services,我们放置具有我们的业务或中间件逻辑的模块。在这里,我们创建了一个服务对象来处理我们的待办事项列表的业务逻辑:todo.js文件。
在 main.js 中,我们导入并添加我们的插件到应用对象,使用 .use(Modals) 方法注册我们的插件。
App.vue文件已经变成了一个主要的布局组件,没有其他应用程序逻辑。我们导入并使用一个头部组件(MainHeader.vue)和一个父组件来管理我们的待办事项和 UI,(ToDoProject.vue),就像图 4.2中所示的设计一样。
ToDoProject组件通过反应变量包含列表的状态,其中我们有以下内容:
-
_items是一个包含我们的待办事项的数组 -
_item是一个辅助反应变量,我们用它来创建新项目或编辑项目的副本 -
_filter是另一个辅助反应变量,我们用它来输入一个字符串以过滤我们的列表
值得注意的是,我们还声明了一个常量$modals,它接收注入的Modals对象 API。注意showModal()函数如何使用此对象打开和管理新项目和编辑项目的对话框结果。然后,相关的模态在模板中显示,通过注释的结尾标记。通常,将所有模态模板放置在组件的末尾,而不是分散在整个模板中。
ToDoProject组件通过 props 将状态数据委派给子组件以显示摘要和列表项。它也从它们那里接收事件,带有操作列表的指令。你可以将这个组件视为功能的根。我们的应用程序只有一个,但这开始暗示了网络应用程序如何通过功能开始组织。
另一个值得注意的点,是使用服务对象和类。在我们的应用程序中,我们有todo.js,我们在需要的地方将其导入为todoService。在这种情况下,这是一个单例,但它也可以是一个类构造函数。请注意,它不包含任何接口逻辑,只有应用程序或业务逻辑。这是区分它与之前看到的组合式的一个决定性因素。
另一个变化是待办事项现在有多个状态,我们可以通过点击在它们之间循环。我们在服务的toggleStatus()函数中实现了这个逻辑,而不是在组件中。状态之间的流程可以表示如下:
图 4.7 – 一个循环有限状态机
你可能认出了这个设计,因为它代表了一个switch语句,就像我们例子中的那样:
Todo.js
[function] toggleStatus(status){
switch(status){
case "not_started": return "in_progress"
case "in_progress": return "completed"
case "completed": return "not_started"
}
}
这个函数,根据其当前状态,将返回下一个状态。在每次点击时调用此函数,我们可以以干净的方式更新每个项目的状态。
关于这个新实现的最后一个要点是ToDoSummary组件中计算属性的用法。我们使用它们来显示具有不同状态的项目的摘要卡片。注意反应性工作得有多好——当我们改变列表中项目的状态时,摘要会立即更新!
新的实现已经有序进行,现在是时候退一步,用批判性的思维审视我们的工作了。
对我们新的待办事项应用的小批评
与我们的第一次方法相比,这个新的待办事项应用版本是一个明显的改进,但它还可以改进:
-
我们仍然只有一个任务列表。
-
所有的操作都只在一个页面上发生。
-
我们的项目是短暂的。当我们关闭或刷新浏览器时,它们就会消失。
-
没有安全性,没有多用户的方式,等等。
-
我们只能添加纯文本。图像或富文本怎么办?
-
经过一些工作,我们可以扩展我们的应用程序,使其管理多个项目、额外内容、分配等等。
-
我们已经取得了良好的进展,但仍有许多工作要做。
摘要
在本章中,我们深入研究了组件,学习了它们如何在框架内进行通信、共享功能以及实现设计模式。我们还看到了将粗糙草图或详细设计转化为组件的方法。然后我们了解了特殊组件,使用框架的依赖注入创建了一个模态对话框插件,并应用其他模式使我们的编码更加容易和一致。此外,我们对应用程序进行了重构,扩展了其功能,同时审视了更好的状态管理,独立于我们之前使用的 HTML 元素。我们已经取得了良好的进展,但仍有许多工作要做。
在下一章,我们将使用到目前为止所学的内容创建一个单页应用(SPA)。
复习问题
回答以下问题以测试你对本章知识的掌握:
-
我们如何从一个视觉设计或原型开始,并使用组件来规划实现?
-
组件之间可以以多少种方式相互通信?
-
我们如何在多个组件中重用代码?还有其他方法吗?
-
插件是什么,我们如何创建一个?
-
我们在新的待办事项应用中应用了哪些模式?
-
你会在实现中做哪些改变?