构建你自己的-JavaScript-框架-全-
构建你自己的 JavaScript 框架(全)
原文:
zh.annas-archive.org/md5/bcd9f325123427860a5236d7529a4568译者:飞龙
前言
这本书是关于构建、理解和维护 JavaScript 框架的——这是许多现代网络应用的关键构建块。在过去 10 多年中,软件框架的生态系统,尤其是 JavaScript 框架,对许多开发者来说都极具吸引力。此外,JavaScript 可以说是最有影响力和最普遍的编程语言,它使数百万关键网络应用和服务成为可能。得益于充满活力的开发者社区和创新公司,JavaScript 框架的发展已经变成一个充满活力和令人兴奋的空间,充满了创新和探索的机会。每个新的框架都带来了自己的哲学和方法,代表了对常见的网络应用和服务的挑战的独特解决方案集。
重要的是要注意,这本书不仅仅是为那些计划从头开始开发他们的 JavaScript 框架的人准备的。虽然它提供了创建良好维护系统的宝贵智慧,但其主要目标超越了这一点。它还旨在提高你对底层框架的理解,提高你的 JavaScript 或软件开发技能。它鼓励你从框架的角度思考,从而培养对可重用软件架构的更深入理解。无论你是寻求深入了解工具集的经验丰富的开发者,还是寻求了解框架景观的新手,这本书都将对你的职业生涯大有裨益。
这本书面向谁
如果你是一个 JavaScript 新手或想要深入探索 JavaScript 框架世界的专家,我们为你提供了全面的支持。这本书介绍了前端框架的历史,并指导你创建自己的框架。为了从这本书中学习,你应该对 JavaScript 编程语言有良好的理解,并对现有的软件框架有一些经验。
这本书涵盖的内容
第一章,不同 JavaScript 框架的优势,详细介绍了 JavaScript 框架的世界及其在过去十年中的发展。
第二章,框架组织,提供了关于框架结构和组织模式的见解。
第三章,内部框架架构,探讨了各种现有框架的核心技术架构模式。
第四章,确保框架可用性和质量,解释了一系列技术和工具,使开发者能够构建专注于可用性和易用性的高质量框架。
第五章,框架考虑因素,探讨了围绕项目目标、技术问题空间和架构设计决策的考虑因素。
第六章, 通过示例构建框架,解释了如何根据模式和最佳技术开发一个简单的 JavaScript 测试框架。
第七章, 创建全栈框架,提供了开发新应用程序框架的实践,该框架将使开发者能够构建大型和小型 Web 应用程序。
第八章, 构建前端框架,特别关注全栈框架的前端组件。
第九章, 框架维护,讨论了与框架发布流程、持续开发周期以及大型框架项目的长期维护相关的话题。
第十章, 最佳实践,解释了围绕通用 JavaScript 框架开发的几个重要主题,并展望了该生态系统框架的未来。
要充分利用本书
您需要对 JavaScript 编程语言有良好的理解,并有一些使用现有软件系统(如 React)的经验。
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| ECMAScript 2023 | Windows, macOS, 或 Linux |
| TypeScript | |
| Node.js 20 |
按照指南在您的机器上安装 Node.js 版本 20+,以便能够与代码文件交互。Node.js 运行时的安装程序可以在nodejs.org/en/download找到。
如果您使用的是本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub(github.com/PacktPublishing/Building-Your-Own-JavaScript-Framework)下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们!
使用约定
本书使用了多种文本约定。
文本中的代码: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“例如,如果您计划在 Next.js 项目中添加 API 路由来构建 API,它们必须映射到/api/端点。”
代码块设置如下:
pages/index.vue
<template>
<NuxtLink to="/">Index page</NuxtLink>
<NuxtLink href="https://www.packtpub.com/" target="_blank">Packt</NuxtLink>
</template>
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
<root framework directory>
| <main framework packages>
+ <core framework interfaces...>
+ <compiler / bundler>
| <tests>
+ <unit tests>
+ <integration and end-to-end tests>
+ <benchmarks>
| <static / dynamic typings>
| <documentation>
| <examples / samples>
| <framework scripts>
| LICENSE
| README documentation
| package.json (package configuration)
| <.continuous integration>
| <.source control add-ons>
| <.editor and formatting configurations>
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“按下调试按钮后,您将看到一个包含项目中所有可用脚本的上下文菜单。”
小贴士或重要注意事项
如此显示。
联系我们
我们始终欢迎读者的反馈。
customercare@packtpub.com并在您的邮件主题中提及书名。
勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。
copyright@packt.com并提供链接到该材料。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《构建您自己的 JavaScript 框架》,我们很乐意听到您的想法!请点击packt.link/r/1804617407为此书提供反馈。
您的评论对我们和科技社区都至关重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买这本书!
您喜欢在路上阅读,但无法携带您的印刷书籍到处走?
您的电子书购买是否与您选择的设备不兼容?
请放心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠不会就此停止,您还可以获得独家折扣、时事通讯和丰富的免费内容,每天直接发送到您的收件箱。
按照以下简单步骤获取福利:
- 扫描下面的二维码或访问以下链接

packt.link/free-ebook/9781804617403
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他福利发送到您的邮箱
第一部分:JavaScript 框架的景观
前四章深入探讨了现有框架的生态系统。这种方法为 JavaScript 框架当前的技术景观提供了独特且多样化的视角。这些章节强调了结构、设计和保证项目卓越性和开发者友好性的方法论。在这里积累的知识使开发者能够理解框架开发的更广泛背景和基本原理,目标是建立一个坚实的基础。
在本部分,我们涵盖了以下章节:
-
第一章, 不同 JavaScript 框架的优势
-
第二章, 框架组织
-
第三章, 内部框架架构
-
第四章, 确保框架可用性和质量
第一章:不同 JavaScript 框架的好处
自从 JavaScript 首次被引入我们的网络浏览器以来,已经过去了 25 多年。从那时起,这项技术极大地改变了我们与网站和应用程序互动的方式,我们为后端系统构建 API 的方式,甚至我们与硬件平台通信的方式。JavaScript 已经成为地球上最受欢迎的编程语言之一。时至今日,JavaScript 的演变速度和快速变化仍然是开发者之间热门的讨论话题——它是兴奋和创新的源泉。作为一种编程语言,JavaScript 在过去 10 年中连续 10 年被开发者评为最受欢迎的语言,并且是 98% 的网站客户端脚本的关键。我们无法低估 JavaScript 及其紧密相关的 ECMAScript 标准化如何使网络成为可以由数十亿人访问的下一代软件的平台。有了这些技术,数百万的企业和个人可以轻松构建出色的应用程序、创意体验和复杂的软件解决方案。在许多方面,网络平台有潜力成为全世界最充满活力和友好的开发者生态系统。
JavaScript 框架是数百万网络开发者今天构建项目的直接方式。由于它们的流行和易用性,框架允许开发者快速将产品想法变为现实,而无需不必要的开销。如果没有我们今天可用的框架系统,网络将无法与其他开发平台竞争。
在这本书中,我们将研究庞大的生态系统,并扩展我们的知识,以便在创建和维护我们自己的自开发框架时充满信心。掌握构建框架或扩展现有框架的技能,将使我们成为前端和后端项目中具有影响力的领域专家。
作为成为 JavaScript 框架专家的一部分,我们需要了解网络开发工作流程的核心组件和工具。在本书的第一章中,我们将探讨网络开发是如何演变的,框架是如何改变与 JavaScript 一起工作的格局的,以及当前生态系统提供了什么。
我们将涵盖以下主题:
-
JavaScript 框架的出现
-
代码库的演变
-
JavaScript 中的框架类型及其好处
-
我与框架的经历
技术要求
本书附有 GitHub 仓库 github.com/PacktPublishing/Building-Your-Own-JavaScript-Framework。在每一章中,我们将指向该仓库中的相关目录。您可以随意克隆或下载该仓库的 ZIP 文件。
您需要一个带有互联网访问权限的桌面或笔记本电脑以及终端应用程序来安装和运行此存储库中的代码。我们还将使用 Node.js 来运行存储库的一些部分。Node.js 是一个开源的、跨平台的、后端 JavaScript 运行环境,可以在浏览器外运行 JavaScript 代码。Node.js 的安装信息可以在nodejs.org找到。对于存储库中的代码,您可以使用任何支持终端和运行 Node.js 的环境,例如 Windows、macOS 和大多数 Linux 版本。
JavaScript 框架的出现
随着 JavaScript 的进步和演变,那些在语言中投入大量精力,无论是公司还是个人,的创新者开始编写软件库来帮助解决 Web 应用程序架构的日常负担。最基本的 JavaScript 库的初始重点是提供单一功能、交互性和附加功能,这些功能逐渐增强了网页。当时,JavaScript 通过其交互组件赋予了静态页面生命——总是浮现在脑海中的简单例子是微小的脚本,这些脚本使得创意按钮效果和鼠标光标效果成为可能。在许多情况下,这些脚本与网站的核心功能是分开的,并且不是用户与内容交互所必需的。自从小型库的诞生以来,这些库为今天我们所拥有的复杂框架系统铺平了道路。前端技术迅速发展,现在,开发者们更加习惯于数兆字节的脚本驱动前端代码。
JavaScript 库是 Web 开发演变的下一步,帮助解决跨浏览器怪癖、复杂视觉效果、网络请求和网页布局。使用这些库,开发者能够控制跨浏览器开发挑战。CSS 开始关注布局功能和跨浏览器标准,改善了 Web 的样式功能。开发者最终开始将结构和精心设计的系统引入 Web 开发。
专注于构建可扩展且具有意见的 Web 软件的时代终于到来了,这正是我们开始看到复杂软件范式被引入大型网站和 Web 应用的曙光。公司和大型企业开始将 Web 视为一个严肃的应用平台,这导致了一些以 JavaScript 编写并从 Java 等语言编译到 JavaScript 的突出项目。回顾到 2009 年末,我们看到了完全使用 HTML、CSS 和 JavaScript 构建的第一个模型-视图-控制器(MVC)框架的迭代。这种 MVC 模型允许更广泛的项目保持组织性,丰富了开发工作流程,并为期望在编写软件时采用更结构化方法的开发者打开了前端开发的世界。MVC 模型与 Web 应用配合得足够好,以至于催生了框架开发的复兴。
许多开发时间都投入到了连接 JavaScript 引擎和浏览器 Web API 之间的机制中。在图 1.1 中,我们可以看到这种交互发生的简化视图:

图 1.1:JavaScript 引擎与 Web API 之间的交互
框架代码及其内部技术,如虚拟 DOM,使用 DOM 及其组件来使 Web 应用体验成为可能。Web 有其自己的 MVC 架构方法,其中 DOM 和 Web API 事件与 JavaScript 中定义的控制器交互。控制器与用 HTML 或模板 HTML 编写的视图接口。此外,在这个范式下,应用组件利用模型来模拟其内部的数据。使用这种方法,我们可以在以后与后端服务通信,以特定方式检索数据。
每个新的 JavaScript MVC 框架都试图以各种方式完善其实现或方法。在第一个 MVC 框架出现并获得普及后的约五年,几个专注于观察者软件设计模式的新范式开始在 JavaScript 社区中受到关注。这种观察者方法是一种软件设计模式,其中对象维护一个其依赖者的列表,称为观察者。对象会自动通知观察者其内部状态的变化。那时,Flux应运而生,这是一种专注于简化 MVC 内部障碍的应用架构。这些障碍包括处理视图不断需要与模型交互、难以调试的深层嵌套逻辑,以及需要为复杂应用提供充分的测试解决方案。
在观察者模式中,我们定义了包含一组通知状态变化的观察者的主题。Flux 架构将这个现有的模式扩展,以更好地适应基于 Web 的应用程序。在 Flux 模式的情况下,它包括与组件状态交互的存储。这些存储根据来自视图中用户的动作产生的数据由调度器通知。许多 JavaScript 框架开始采用这种模式,最终简化了工程师构建应用程序的方式,同时仍然强制执行一套适用规则以保持关注点的分离。所有这些框架中的软件模式在接口、数据模型和整合它们的程序逻辑之间提供了清晰的关注点分离。基于 Flux 的 JavaScript 框架引入了从已知 MVC 模式中出现的新的概念。然而,MVC 和 Flux 方法都侧重于应用程序开发中的关注点分离原则。
此外,在简化 Flux 提出的思想的同时,一个名为 Redux 的库启发了下一代框架改变它们的应用状态管理方法。与 Flux 调度器不同,Redux 框架依赖于一个单一的存储,使用纯reducer函数,接受当前状态并返回一个更新后的状态。即使今天,前端模式仍在成熟,为 Web 平台构建正变得越来越容易。
尽管有很多关于前端技术的方面可以提及,JavaScript 也在 Web 浏览器之外的地方产生了巨大的影响。我们将在下一节中介绍这些领域。
浏览器外部的框架
在第一个前端框架出现期间,另一个具有里程碑意义的事件是出现了一个名为 Node.js 的新开源运行时。Node.js 允许开发者使用 JavaScript 来生成服务器端脚本、部署后端系统、构建开发者工具,更重要的是,使用与浏览器相同的语言编写框架。软件栈两端的 JavaScript 的独特组合为软件开发者创造了巨大的可能性。这个运行时随后扩展到许多超出软件应用的方向,包括桌面应用程序开发、硬件 I/O 解决方案等等。
使用 JavaScript 构建的框架使得网络平台成为数十亿人可触及的最重要技术之一。几乎无法想象在没有依赖框架的一致性和友好性的情况下开始一个新项目,即使是微小的任务也能从使用一个统一且具有观点的结构中受益显著。然而,尽管语言和构建网络项目的方式发展迅速,JavaScript 框架作为完全封装的平台来帮助开发者生产高效应用,这一过程还是花费了相当长的时间。
随着 mobile 平台的发展,JavaScript 取得了胜利,为移动和现有系统创建了多个框架,并将移动基准测试整合到其发布流程中。优化已经达到了硬件级别,ARM (arm.com) 处理器架构引入了优化,以改善数据类型转换中的 JavaScript 性能,从而为许多 JavaScript 应用程序带来性能提升。这对于一个始于普通网页上的小脚本的脚本语言来说,确实是一段漫长的旅程。
今天,我们可以通过结合 Web API 的力量、JavaScript 语言以及像渐进式 Web 应用这样的技术,使用将这些所有元素整合在一起的框架,来创建完整的应用和服务。这是开始探索这些 JavaScript 系统的世界并利用它们为我们带来优势的绝佳时机。
现在,我们已经对网络开发的发展有了概述,让我们来看看代码库是如何随时间变化的。
代码库的演变
在学习框架的同时,回顾构建网络的方式是如何随时间变化的,是非常有趣的。这种探索帮助我们理解为什么我们今天以这种方式构建 Web 应用程序,并帮助我们从历史转变中学习。它还允许我们在承担大型项目时,对框架的可用性和开发决策更加谨慎。随着技术的进步,围绕网站和 Web 应用程序构建的要求和期望发生了巨大变化。根据一个人参与 Web 开发的时间长短,他们要么经历了代码库结构快速演变的许多转变,要么幸运地避开了工具和流程非常繁琐的时期。
最初,代码库由独立的客户端组件拼接而成,包含代码重复和编码模式的混合。代码组织、软件开发模式的运用和性能优化并不是开发者关注的重点。Web 应用程序的部署过程也相对简单。在许多情况下,网站是手动更新的,没有使用源代码控制或版本跟踪。测试也高度手动,并且仅在少数足够大的项目中存在,以便进行测试。这还是在部署管道、持续集成、部署自动化和高级测试基础设施严格验证每个变更之前。曾经有一段时间,开发者必须为了性能原因优化他们的 CSS 选择器。
幸运的是,随着行业开始更多地关注构建复杂应用程序,生产力和工作流程迅速开始提高。今天,我们有源代码控制,我们有众多测试和部署工具可供选择,我们建立了软件范式,这些范式大大提高了我们的生活质量,并极大地提高了我们构建的项目质量。JavaScript 引擎的改进为框架开辟了新的途径,而网络浏览器的改进则通过如 虚拟 DOM、Shadow DOM 和 Web Components 等技术解决了慢速 DOM 交互问题。如今,前端框架有更好的客户端平台可以针对,而更成熟和改进的网络标准使得执行更复杂的操作成为可能。例如,借助 WebAssembly (webassembly.org) 标准,我们可以在浏览器中运行低级代码,并实现性能的提升。
作为所有这些发展和流行度增长的一部分,Web 应用程序开发工作流程在许多方面变得更加复杂。几乎在每次与 Web 应用程序项目互动的每个点上,都有一个工具集旨在改进我们的工作流程。这方面的例子包括 Git 源代码控制、各种文件的前后处理器、带有插件的代码编辑器、浏览器扩展等。这里有一个示例,说明了现代 Web 应用程序代码库结构的关键组件,在这种情况下,是由 SvelteKit 生成的:

图 1.2:SvelteKit 代码库结构
我们将在本章的“使用 React 的框架”部分稍后介绍 SvelteKit,即使你从未使用过 Svelte,如果你与其他框架一起工作,这个项目文件结构看起来也会非常熟悉。这种工具的动态结构使得在更换某些功能时具有灵活性。例如,如果需要,可以用另一个代码格式化工具替换 Prettier,而项目的其余结构保持不变,并按原样运行。
随着 JavaScript 中第一个框架的建立,我们经历了构建步骤被引入到我们的项目中,这意味着外部或捆绑的工具将帮助运行或构建应用程序。今天,这个由Webpack或esbuild普及的构建步骤几乎无法避免。作为构建步骤的一部分,我们使用包管理器获取应用程序依赖项,处理 CSS,创建代码包,并运行各种优化步骤,使我们的应用程序运行得更快,消耗最少的带宽。生态系统还引入了 JavaScript 转译器,这是一种源到源代码的编译器。它们用于将一种特定的语法(可能包含更现代的功能或包括额外的功能)转换为广泛接受的 JavaScript 语法。转译器,如Babel,开始被日常使用,并在许多项目中与构建步骤集成;这种模式通常促使人们使用最新的语言功能,同时也支持旧的浏览器引擎。如今,转译和构建步骤不仅适用于 JavaScript 文件,还适用于 CSS 和特定的模板格式文件。
与构建步骤集成的是包管理器,如npm或yarn,它们在解决项目依赖关系方面发挥着至关重要的作用。如果你想使用框架启动工作流程,你可能会依赖包管理器来初始化框架结构和其依赖项。对于新项目来说,如果不使用包管理器或某种形式的依赖关系解析,几乎不可能有一个合理的框架工作流程。随着项目的增长,包管理器有助于组织新的依赖项,同时跟踪已使用模块的更新。如今,文本编辑器,如 Visual Studio Code 和 IntelliJ WebStorm,适应我们的代码库,并提供出色的工具,使我们能够实现代码的源代码控制。这些编辑器依赖于内置功能和外部插件,鼓励更好的格式化、更简单的调试以及针对特定框架的改进。
随着技术的进一步发展,代码库将不断变化,工具也将持续改进,以便我们能够更快地开发应用程序。关于框架组织,我们可以期待更高层次的抽象,这将简化我们的 Web 开发方式。许多编程语言,如 Java 和 Swift,都有预定义的开发工作流程,封装了开发的所有方面。迄今为止,JavaScript 代码库一直是这些规则的例外,并允许高度灵活性。随着 Web 开发工具和创新的快速步伐并未放缓,这一趋势还将持续许多年。
现在我们已经了解了 JavaScript 生态系统是如何演变的,以及代码库是如何随时间变化的,让我们来探讨 JavaScript 框架在前端、后端、测试等方面的提供内容。
JavaScript 框架的类型及其好处
尽管在生态系统中比较每个框架的细微差异具有挑战性,但我们仍可以涵盖几个对开发者社区产生重大影响或提供独特解决方案的框架。对工具的深入了解帮助我们注意到这些框架中关于开发者体验和功能集的不同策略的具体模式。
仍然有方法在不使用框架的情况下构建应用程序和网站,但许多开发者即使考虑到额外的开销和学习曲线,也更倾向于使用成熟且具有观点的框架。如果你关注 JavaScript 社区,你会发现它总是充满热情地讨论着框架,因此让我们更深入地探讨框架使用的需求和好处。
框架提供了良好的抽象级别,可以在不重写底层功能的情况下编写高级代码。开发者可以更多地参与到业务和产品逻辑中,并更快地迭代新功能。以一个例子来说,直到最近,在没有良好抽象的帮助下编写带有适当错误处理的异步 Web 请求的代码是一项非常耗时的工作。现在有了 Fetch API (fetch.spec.whatwg.org),这变得容易多了,但 Fetch 只是故事的一部分,所以其他 Web API,尤其是早期的一些,仍然受益于良好的抽象。在我们选择编写底层代码的情况下,找到在框架边界内编写该代码的方法是一个更好的方法。这样,代码就在框架的基本原理中得到测试和维护。这避免了额外的维护,并确保所有该代码的使用都仍然在合理的抽象之下。一些后端框架通过提供可扩展的接口来实现这一点,通过插件或扩展默认行为来钩入框架内部。
与团队一起开发软件是一项具有挑战性的任务,因此小型和大型团队都可以从将框架集成到工程工作流程中受益。提供的抽象结构通常会导致更完善的系统,考虑到开发者编写高级组件的限制。关键好处是使所有参与任务的人都能更好地理解代码库,并方便地减少在重构和添加新代码上的深思熟虑时间。
现在我们有了抽象的高级代码,我们可以珍惜框架的另一个好处——它们所提供的性能优化。编写适用于所有提供用例的性能代码需要技能,并且会从当前项目上消耗大量时间。即使是最有知识的开发者,在短时间内也只能提出足够好的解决方案。有了框架,尤其是开源框架,你可以从许多人的智慧中受益,解决性能瓶颈,克服典型障碍,并在框架发展的过程中继续受益。性能优势来自于优化的低级和结构良好的高级组件;值得注意的是,一些框架会防止降低应用程序速度的代码。
框架使得与外部系统(如数据库、外部 API 或特定组件)的集成更加容易。例如,一些 Web 框架可以直接与 GraphQL 数据查询语言集成,简化后端系统的交互。这不仅是因为使用方便,而且这些集成使得与数据库等组件的安全交互成为可能,有助于避免可能执行缓慢或有害的查询。对于前端项目,始终跟上最新的 Web 标准非常重要,这正是框架提供另一个集成优势的地方。
最后,就像所有软件一样,支持发挥着重要作用。一个项目可能使用成熟框架的另一个原因是可用的支持渠道,包括付费、志愿者和开源的帮助。这些系统的共享知识使得开发者能够互相帮助构建系统,并使得雇佣熟悉这些现有系统的新开发者变得更加容易。
正如我们所见,框架以无数的方式为我们带来好处——让我们用这些确切的原因来回顾一下。以下是框架允许我们做的事情:
-
专注于业务逻辑和编写高级代码
-
编写更少的代码并遵循框架定义的代码约定
-
从性能提升中受益并依赖未来的优化
-
以良好的架构、抽象和组织开发项目
-
容易与数据库和外部 API 等外部系统集成
-
能够依赖安全修复、审计和补丁
-
使用框架特定的工具(如文本编辑器集成和命令行实用程序)改进开发者工作流程
-
能够通过依赖详细的错误信息和一致的日志轻松调试问题
-
依赖框架作者和社区的额外支持
-
招聘已经习惯于使用我们选择的框架或具有类似经验的开发者
-
通过利用框架的功能集开发更好的用户体验
虽然许多 JavaScript 框架专注于开发者体验,但用户体验有时会因这些系统的开销而受到影响。这通常在前端项目中相关——一个例子就是在预算有限的移动设备上加载复杂的 Web 应用程序。在后端系统中,这可以在 API 无法跟上请求负载并可靠地与流量峰值同步时看到。
即使在这两种情况下系统都巧妙地构建,所选框架可能并不优化以覆盖所有用例。我相信框架生态系统的下一版本将主要关注用户体验方面,这意味着加快加载时间,减少通过网络传输的 JavaScript,并确保我们创建的 Web 应用程序能够在所有平台上无缝运行。在接下来的章节中,我们将探讨一些最受欢迎的框架,这些框架为 Web 应用程序开发者提供了这些好处。
前端框架
由于 JavaScript 框架起源于浏览器,让我们将现代前端框架作为我们的第一次探索。
Ember.js
假设我们通过Prototype.js、jQuery和script.aculo.us等库的起源追溯第一个 JavaScript 框架的根源。在这种情况下,我们最终会到达SproutCore,这是一个被苹果公司和少数其他公司用来在多年前构建一些最复杂 Web 体验的框架。
今天,这个早期的 SproutCore 项目已经影响了Ember.js框架。Ember 继续是一款具有高度意见的软件,允许我们使用定义良好的组件、服务、模型和强大的路由器来构建应用程序。像本章中我们将讨论的许多框架一样,Ember 自带命令行工具,这有助于开发者快速入门应用程序的基础,并在项目范围扩大后快速生成更多代码。提供的框架工具的实用性巨大。CLI 封装了代码生成步骤,并允许运行常见的框架命令,例如运行测试或提供应用程序文件。使用 Ember,开发者可以获得一套完整的工具,如自动重新加载、浏览器开发者工具以及一个名为 Ember Data 的包,该包通过适配器和序列化器帮助管理 API 到模型的关系。最终,Ember 的学习曲线比其他框架更陡峭,但其高度意见的概念引导开发者构建高度功能的 Web 应用程序。
Angular
Angular 是另一个拥有大量追随者的框架。以其核心的 TypeScript 为基础,它通常被用作其他全栈 Web 框架的子集系统。Angular 提供了其基于组件架构的见解方法。Angular 有一个复杂的重写历史,但现在它是一个更精简的项目,具有稳定的特性集。Angular 的模板语法通过添加表达式和新属性扩展了 HTML。在其核心,它使用依赖注入的模式。该框架的最新版本提供了各种绑定技术,包括事件、属性和双向绑定。
Vue.js
Vue.js,也是用 TypeScript 编写的,是通过借鉴 Angular 的优点而创建的。开发者喜欢 Vue 在其组件系统、语法和一般使用上的简单性。它利用了 模型-视图-视图模型(MVVM)模式,其中视图通过某种数据绑定技术与视图模型进行通信。在 Vue.js 的例子中,对于其数据,它通过 HTML 类、HTML 元素和自定义绑定元素属性使用不同的技术来实现这一点。给定视图模型的目的在于处理尽可能多的视图交互逻辑,并在表示逻辑和应用业务逻辑之间充当中间结构。除了使用 HTML 编写,Vue 还具有 单文件组件(SFC)格式(vuejs.org/api/sfc-spec.html),将组件的所有方面——脚本、样式和模板——封装到一个文件中。SFC 作为构建步骤的一部分发生,有助于组件避免运行时编译,将 CSS 样式限制在组件范围内,启用热模块替换,以及更多。
关于 TypeScript
.ts 和 .tsx 是 TypeScript 文件,必须在大多数环境中编译成 JavaScript 才能使用。
使用 React 的框架
这些天,我们经常听到关于 React 的消息;尽管它本身是一个用户界面组件库,但它已成为许多前端框架的基石,例如 Gatsby、Remix、Next.js 以及其他一些框架。作为其介绍的一部分,React 还推出了 JSX,这是它自己的 JavaScript 扩展集,使得可以使用类似 HTML 的语法来定义组件。例如,静态站点框架 Gatsby 依赖于 React 的状态管理和嵌套组件架构来组合其网页。使用 Gatsby,开发者可以通过 GraphQL 从内容管理系统、电子商务来源和其他地方多路复用数据。
沿着我们的 React 路线,我们来到了 Remix,它捆绑了一个全栈解决方案,包括服务器和客户端的功能,以及编译器和请求处理器。Remix 为应用的视图和控制器方面提供了解决方案,并依赖于 Node.js 模块生态系统来处理其余部分,为需要从项目到项目定制解决方案的开发者提供了灵活性。基于多年来创建和维护 react-router 项目的经验,Remix 的创造者能够在利用浏览器 Web API 的同时,避免了投资于新概念,从而提出了强大的抽象。例如,如果你选择 Remix 作为你的项目,你将发现自己比在其他一些框架中更多地使用 Web 标准 API。
Next.js 是我们的下一个基于 React 的框架,它通过内置的服务器端渲染扩展了 React 组件架构的使用。服务器端渲染的组件允许将预渲染的页面发送到客户端,从而使客户端只需在初始化交互式组件上花费资源。该框架提供了页面概念,允许使用懒加载进行更简单的路由实现,并启用自动代码拆分。结合所有这些功能,结果是一个具有快速加载时间的出色用户体验。此外,部署的应用程序在搜索引擎索引中排名很高,这是使该框架脱颖而出的特性。
当谈到 React 框架时,值得提及 Solid.js。这是一个较新的库,用于创建前端界面。Solid 的基准测试优于 React 和其他库。它使用 JSX 等特性,但有一些关键区别。在 Solid 中,没有虚拟 DOM 和 hooks 的概念。相反,它依赖于 信号 模式来更新真实的 DOM 节点,同时利用响应式原语。作为 Solid 方法的一部分,它提供了 SolidStart 应用框架,这与 Next.js 非常相似。它由核心支持组件组成 – 路由器、会话、文档、动作、数据、入口点 和 服务器 – 这些组件作为 SolidStart 的一部分集成在一起。
SvelteKit
与 SolidStart 类似,也存在 .svelte 文件,这些文件封装了带有 <script>、<style> 和 HTML 标签的组件。这些组件被编译成由编译器生成的 JavaScript 输出。
关于 Vite
vite.config.js 配置文件。主要用作前端项目的构建工具。它针对速度进行了优化,并通过提供具有热模块替换的开发服务器以及使用 esbuild 优化 JavaScript 输出的打包器来实现这一速度(esbuild.github.io)。
框架特性和模式
要理解大多数现代框架能实现的功能,我们需要了解以下缩写和特性:
-
单页应用(SPA):一个早期术语,描述了一个仅使用 JavaScript 和其他前端框架进行所有交互,并减少浏览器路由的应用程序。
-
服务器端渲染(SSR):在服务器端预渲染的组件,在客户端传输以进行 JavaScript 激活。
-
客户端渲染(CSR):使用 JavaScript 在浏览器端纯渲染组件。
-
静态站点生成器(SSG):预先从源生成所有页面以实现更快渲染和更好的搜索引擎优化的概念。
-
延迟静态生成器(DSG):在服务器接收到请求时在服务器上渲染内容。
-
增量静态再生(ISR):另一种静态内容生成的模式。在这种情况下,静态生成是由外部触发器更新触发的。
-
内容安全策略(CSP):用于提供脚本的配置,有助于防止跨站脚本攻击。
-
热模块替换(HMR):在浏览器中运行应用程序时替换 JavaScript 模块的技术,主要用于提高开发速度并避免页面重新加载。
-
单文件组件(SFC):一种封装了可使用框架组件所有方面的文件结构,如样式、模板、逻辑等。
-
模型-视图-控制器(MVC):一种关注于各种类型应用中关注点分离的设计模式。它通过以下方式实现这种分离:一个表示数据的模型,一个提供用户界面的视图,以及作为视图和模型之间中介的控制器。
-
模型-视图-视图模型(MVVM):另一种关注于应用程序中关注点分离的设计模式,但实现这些分离的方法不同。在这种情况下,仍然有视图和模型,类似于 MVC。然而,ViewModel 充当这些类型之间的连接。这种方法使用视图和模型之间的双向数据绑定。
除了功能和它们的缩写,这里有一个有用的视觉描述,描述了 MVC 和 MVVM 模式:

图 1.3:MVC 与 MVVM 模式对比
在前端框架复兴期间,一个名为TodoMVC的开源项目被建立起来,旨在帮助开发者根据相同的待办事项应用比较框架,任何人都可以发送带有他们框架实现的拉取请求。除了比较不同的框架外,该项目还普及了 JavaScript 复杂代码组织的方法。现在随着这些新框架的出现,我们需要对 TodoMVC 进行新一轮迭代,以继续帮助开发者比较这些系统。
后端框架
从前端切换到后端,让我们来看看一些后端框架。Node.js 在 JavaScript 生态系统中扮演着至关重要的角色,它为开发后端服务提供了各种框架。与前端一样,不可能涵盖所有这些框架,但在这个部分,我们将检查hapi.js、express、Sails.js、nest.js和AdonisJS。
Hapi.js
在多年的框架探索中,我有机会以专业身份和在小型的爱好项目中使用这些框架。我首先从 hapi.js 开始,它是 Node.js 框架精心制作的良好例子,它通过提供必要的默认设置,使得快速构建服务器后端成为可能。它采用了一种独特的方法,避免使用中间件并依赖于外部模块。作为其核心的一部分,它已经内置了验证规则、解析、日志记录等。hapi.js 不限制可扩展性;开发者可以创建插件并将它们注册为请求生命周期不同部分的执行部分。hapi.js 的使命强调在组合大量应用程序逻辑时避免意外后果。这在 hapi.js 处理依赖管理和模块命名空间的方式中表现得尤为明显。
Express
与 hapi.js 形成鲜明对比的是,Node.js 生态系统还有一个名为Express的框架,它主要是一种无特定观点的后端服务构建方法。成千上万的项目和工具通常使用 Express 进行路由、内容解析和高性能。Express 几乎在所有方面都很灵活,并支持超过一打的模板引擎,是 Node.js 开发者的入门框架。例如,一个流行的 MVC 框架 Sails.js,基于 Express 的功能提供 API 生成、数据库 ORM 解决方案以及构建实时功能的支持。通常,它是对那些欣赏 Express 中间件模式、同时希望采用更结构化方法构建后端系统的人来说是一个好的解决方案。
NestJS
NestJS,不要与 Next.js 混淆,是另一个值得提及的服务端框架。它与 Vue 类似,并且 Angular 启发了其对应用结构的处理方法,但在这个案例中,是针对后端系统的。默认情况下,它使用 Express 作为其默认的 HTTP 服务器,并创建了一个抽象层,允许开发者更换第三方模块,使得开发者可以将 Express 替换为其他 HTTP 框架,例如Fastify。在 NestJS 中,我们看到类似的依赖注入模式,这使开发者能够构建封装的模块。这些模块可以在测试中重用、覆盖和模拟。
AdonisJS
我们本节最后要介绍的 Node.js 框架是 AdonisJS。它完全使用 TypeScript 构建,集成了许多成熟框架应有的特性,例如基于 Active Record 模式的 ORM、模式验证器、广泛的认证支持以及更多。内置和第一方插件特性为后端构建中的许多常规问题提供了解决方案。AdonisJS 还内置了一个自定义模板引擎,用于渲染 HTML 布局。作为额外的奖励,AdonisJS 的文档直接明了,易于阅读和探索。
Fresh
考虑到 Node.js 生态系统对核心框架的关注,我们也应该提一下名为 Fresh 的后端框架,它由 Deno 运行时提供支持。这个运行时结合了多种技术——JavaScript、TypeScript、WebAssembly 和 Rust 编程语言。Fresh 采用了一种简单的方法,强调没有构建步骤、最小化配置,并在服务器上即时渲染组件。路由通过在项目目录中创建文件来处理,称为文件系统路由,这与其他框架中的类似模式。
回顾本节中我们讨论的所有 Node.js 框架,可以看到一个健康的框架多样性,为任何类型的项目提供了解决方案。
原生框架
对 JavaScript 的了解也使我们能够为原生操作系统环境构建,并与硬件平台交互。在其他环境中运行时的可用性使我们能够创建独特的解决方案,帮助网页开发者将他们的技能应用到浏览器以外的领域。在本节中,我们将介绍为原生 JavaScript 开发创建的一些框架。
电子
将 Web 应用打包成原生应用的想法并不新鲜,但通过 Electron 得到了完善。Electron 允许开发者使用熟悉的客户端技术构建能够在流行的桌面平台上运行的完整跨平台应用程序。它支持诸如自动更新和进程间通信等特性,同时还拥有一个利用操作系统功能的插件集合。除了高级框架特性外,有一个针对所有平台的单一代码库对于高效构建新功能和修复错误也是有益的。如今,数百万人在使用由 Electron 构建的应用程序,在很多情况下甚至不知道这一点。例如 Microsoft Teams、Slack、1Password、Discord、Figma、Notion 等应用程序都使用了 Electron。更多示例可以在 electronjs.org/apps 找到。
React Native
另一个帮助我们为原生平台创建应用的框架是React Native,它为熟悉 JavaScript 的开发者打开了移动开发的世界。针对 iOS 和 Android 移动平台,就像桌面上的 Electron 一样,它带来了 React 用户界面构建块的所有好处、统一的代码库和强大、成熟的社区。
Johnny-Five
Node.js 生态系统还提供了如Johnny-Five这样的硬件框架,它允许使用 JavaScript 和 Firmata 协议进行机器人编程的创意学习用例。Johnny-Five 是一个支持超过 30 块硬件板的 IoT 平台。主要提供与 LED、服务、电机、开关等交互的接口。
到目前为止的所有框架都处理构建应用程序逻辑,但在 JavaScript 中还有其他类型的框架在开发过程中扮演着重要的角色——这些就是测试框架。
测试框架
软件开发中的测试框架对于确保我们的项目按预期运行至关重要。随着 JavaScript 及其支持的运行时环境,我们手头上的任务更加艰巨——我们不得不在不同的浏览器引擎中进行测试并模拟原生的 Web API。在某些情况下,模拟内置和外部库也可能具有挑战性。语言的异步行为也带来了自己的障碍。幸运的是,JavaScript 生态系统提出了各种测试框架,以解决许多软件测试挑战——单元测试、集成测试、功能测试、端到端测试等。例如,Jest、Playwright和Vitest都为测试挑战提供了很好的解决方案。我们将在下一节讨论它们。
Jest
随着我们开发 Web 应用程序,我们希望确保我们构建的组件按预期工作;这就是 Jest 这样的框架发挥作用的地方。Jest 是一个与其他项目集成良好的单元测试框架。如果我们得到一个包含我们在本章中看到的一些框架的项目,Jest 将为我们提供可靠的测试解决方案。它设计得非常人性化,配置最小或为零,并提供易于模拟、对象快照、代码覆盖和最重要的是,一个易于理解的 API 来组织我们的测试。
Vitest
Vitest 是一个类似的单元测试框架,提供了与在 Web 项目中模拟模块相同的接口。它专注于速度和对许多框架组件的支持,包括 Vue、React、Svelte,甚至 Web Components。它旨在提高开发者的生产力,并具有智能测试监视模式、多线程测试运行器和熟悉的快照机制。
Playwright
除了单元测试之外,我们的软件项目从端到端测试中受益匪浅;这正是 Playwright 等测试框架成为良好竞争者的地方。它为 Web 应用程序提供跨浏览器和跨平台的测试。Playwright 附带一组测试接口,用于自动化各种浏览器,包括导航到 URL 和点击按钮。由于网页的异步性质,这历来是一个具有挑战性的问题,但这个框架通过重试和 await 行为提供避免不稳定测试的方法。
根据你参与的 JavaScript 项目的需求,你可能需要创建新的测试工作流程或定制现有的测试基础设施以适应你的用例——这正是构建测试框架的经验会带来优势的地方。
框架展示
这里是本章中我们讨论的框架的概述。
这些是我们将在本书中关注的值得注意的 Web 应用程序框架:
| 前端 + 全栈 |
|---|
| 名称 |
| AngularJS |
| Bootstrap |
| Ember.js |
| Vue.js |
| Gatsby |
| Angular |
| Next.js |
| Nuxt.js |
| SolidStart |
| Remix |
| SvelteKit |
图 1.4:前端和全栈框架示例
这些是一些可以作为良好示例并帮助我们学习某些框架开发模式的后端框架:
| Backend |
|---|
| 名称 |
| hapi.js |
| Express |
| Sails.js |
| NestJS |
| AdonisJS |
| Fresh |
图 1.5:后端框架示例
其他使用前端技术针对原生或硬件开发的框架如下:
| Native + Hardware |
|---|
| 名称 |
| Johnny-Five |
| Electron |
| React Native |
图 1.6:原生和硬件框架示例
这里有一些测试框架示例,这些示例在 Web 应用项目中集成和使用非常有用:
| Testing |
|---|
| Jest |
| Playwright |
| Vitest |
图 1.7:测试框架示例
跟踪框架开发当前方向的最好资源之一是 stateofjs.com。这是一年一度的调查,结果来自数千名开发者,它提供了技术发展趋势的展望。例如,如果我们看看 2022 年的前端框架排名(2022.stateofjs.com/en-US/libraries/front-end-frameworks),我们已经开始看到 React 的保留和兴趣缓慢下降,这可能表明行业正在缓慢转向其他解决方案。由于所有这些框架的使用、意识和流行度的持续变化,我们不会专注于许多今天的框架,而是将涵盖适用于未来新框架的核心模式。这些模式将有助于您在探索创建自己的框架时。
现在是时候尝试本章中提到的某些框架了,使用本书“技术要求”部分中提到的 GitHub 仓库。您可以按照以下步骤操作:
-
从 nodejs.org 安装 Node.js 版本 20。
-
从
github.com/PacktPublishing/Building-Your-Own-JavaScript-Framework克隆仓库。 -
使用您的终端,切换到仓库的
chapter1目录。 -
运行
npm install然后运行npm start。 -
按照交互式提示运行示例。
展示重点在于根据框架类型重现相同的示例。所有前端框架都展示了用不同结构编写的相同组件。请注意,一些示例可能需要一些时间来安装和运行。
在本章的最后部分,我们将探讨我在 Web 开发中与框架相关的显著个人经验。
我与框架的经历
我的专业网页开发生涯最初是从构建基本的网站开始的,那时还没有现成的框架或库。展望未来,我想分享我在专业上利用框架的经验,以及为一些开源项目做出贡献的经历。如今,从这些经验中积累的知识帮助我更好地评估框架的实用性,并将新的软件范式引入我的工作中。许多人发现跟上 JavaScript 领域的最新创新具有挑战性,但即使是构建最小的项目也能在许多方面帮助开发者成长。
下面是一些全栈开发领域的例子,这些例子帮助我成为了一名更加高效的网页开发者。
前端开发
我最初构建的几个专业网站是为 Internet Explorer 6 和 Firefox 网络浏览器的早期版本开发的。正如我们从这一章节中学到的,当时还没有用于构建 Web 应用的框架,我不得不利用我所拥有的少数库。这些库可以帮助添加交互性,例如图片库和动态页面布局。幸运的是,当我的关注点转向更大型的项目时,jQuery前端库出现了,并开始流行起来。时至今日,jQuery 仍然是大批网站的首选工具。我有了亲手打造一个基本框架的机会,这个框架可以从一个项目重用到另一个项目。这一系列脚本非常方便,预示了我们今天所拥有的框架的辉煌未来。很明显,单页 JavaScript 应用的趋势正朝着结构化和有观点的解决方案发展。
在我早期的大型项目中之一——具体来说是 Firefox 账户前端(accounts.firefox.com),我有机会使用 Backbone.js,借助 jQuery 和多个扩展库使其更适合大型项目。至今仍在为成百万用户服务的 Firefox 账户前端仍在使用 Backbone.js。Backbone.js 框架的结构允许对 jQuery 的软依赖,所以它感觉像是我在早期网络应用开发方法上的自然延续。我从这次经历中得到的要点是,Backbone.js 并非前端网络应用挑战的完美答案,但在许多方面都有益。例如,它使项目能够保持与不断发展的 JavaScript 生态系统的灵活性,并帮助多样化的开发者遵循一套稳固的应用程序指南来共同工作。有机会在客户端和 Firefox 网络浏览器的集成服务中工作,教会了我如何为全球数百万台计算机上运行的桌面客户端生成 JavaScript 组件。
在许多专业项目中,我有机会与 Ember.js、Angular 以及各种 React 框架合作。在这些场合,我对这些框架的强大功能印象深刻。在我的经验中,值得特别一提的是于 2012 年初发布的 Meteor 网络框架。Meteor 的一个主要卖点是其同构或所谓的 通用 JavaScript 方法,其中代码在客户端和服务器上同时运行。在许多方面,我们今天在流行的框架中看到了类似的方法,其中全栈框架让开发者能够编写 JavaScript 来在栈的两端进行开发。我为这个框架构建了一些应用程序和一些插件,虽然开始使用 Meteor 感觉非常容易,但在尝试构建不符合 Meteor 支持范围的项目时,我遇到了障碍,尤其是在框架的早期版本中。一个特别与框架限制作斗争的例子是在多个客户端之间同步文档状态的功能开发。当时,使用 Meteor 的功能集来实现这一点颇具挑战性,不得不使用替代工具重新构建。幸运的是,这并不是一个关键项目,但在重要的时候,评估你选择的框架是否是你试图构建的正确工具是一个好主意。
后端开发
在 Node.js 的早期几年,我有机会参与几个使用微服务架构的项目,这些项目涉及使用 Express 和 hapi 框架。我感受到了 express 框架的开放性方法与 hapi.js 中定义的严格规则和选项之间的对比。例如,在 hapi.js 中覆盖和自定义某些行为相当困难,而保持框架更新则需要困难的代码库迁移。
我仍然记得逐个版本地检查 hapi.js 的变更日志,确保不会错过任何可能导致我的项目失效的重大变更。尽管 hapi.js 存在困难,但它确实感觉这个框架提供了一套良好的抽象。在许多方面,遵循像 Python 中的 Flask 这样的现有示例,hapi 拥有构建高度可用的服务所需的必要组件。同时,我的 Express 经验似乎更让我想起了 jQuery 和 Backbone.js 的日子。在 Express 项目中,我可以拥有一个高度灵活的开发环境,结合不同的 Node.js 模块来实现框架想要的功能。这让我意识到,对我来说完美的框架应该介于 Express 和 hapi 之间,也就是说,它将允许我保持创造力、高度的生产力,并充分利用运行时生态系统,同时同时拥有一个有强烈意见的框架核心,这将使我的应用程序高效且可靠。
开发者工具和更多
作为我的职业的一部分,我一直对开源充满热情,因此我将我的努力集中在为开发者工具和测试框架做贡献上。多年来,我一直担任Grunt.js(gruntjs.com)的维护者,这是一个 JavaScript 任务运行器。Grunt.js 一直是框架的核心组件,如Yeoman,并且在 AngularJS 的早期版本中被用作首选工具。自从那时起,Node.js 的任务运行器规范已经发生了很大变化,但仍然有相当数量的项目在使用 Grunt.js。维护这个项目多年感觉就像维护一个大型框架项目一样——发布新版本、保持稳定的 API、通过安全赏金支持它以及更多。还有一大堆问题、功能需求、拉取请求和插件需要支持。
在我的测试框架贡献方面,我参与了Intern.js测试框架(github.com/theintern)的开发,它为 Web 应用程序提供了单元和功能测试。在我的日常项目中,我既是这个框架的贡献者也是消费者,这让我对这个项目有了独特的视角。我受到启发,要提供一个良好的集成体验,因为它将有助于我的项目。作为这项工作的一个部分,除了学习如何构建测试框架之外,我还专注于为其他应用程序框架开发集成示例和文档。在提供的示例中涵盖了众多集成场景,这使得开发者将这个测试系统集成到他们的应用程序中变得容易得多。
根据我个人的经验,一个值得注意的框架是voxel.js——一个开源的体素游戏构建工具包。虽然它并不那么流行,但它是一个创造性使用 JavaScript 的绝佳例子,结合了前端和后端技术。这是一个由小型团队构建的框架,为那些想要从事游戏和可视化开发的开发者群体填补了一个空白。voxel.js 并非旨在成为一个改变世界的框架;相反,它为许多人提供了一个创造性表达的途径。在我个人的项目中探索 voxel.js 时,我学到了很多关于独特框架和模块结构的知识,并且尝试那些能够激发更多创造性思维的系统是一件很有趣的事情。
为新项目做出贡献
这些在前端、后端和开发者系统中的 JavaScript 框架经验对我来说作为职业生涯的一部分是极其宝贵的。我学会了遵循最佳实践、遵守软件模式以及为各种运行时环境开发的重要性,这最终帮助我编写了更好的代码并成功交付了项目。作为本书的一部分,我分享了我的学习经验和尽可能多的知识,以便下一代虔诚的 JavaScript 开发者能够构建和贡献他们自己的框架项目。
我参与的项目总是有不同的起源。在我的情况下,我必须与私有和开源框架一起工作。在工作项目中,我专注于将开源工具与更大的商业组织环境相结合。这种方法有助于将现有工具与特定项目的需求相一致。在开源环境中,我有幸为那些改善了整体开发者体验的项目做出贡献。在许多场景中,我有机会参与那些创新且在 JavaScript 生态系统中的首创项目。例如,当 Grunt.js 正在发展时,有来自其他语言的作业运行工具,但 JavaScript 工具还处于起步阶段。为 voxel.js 做出贡献有类似的体验;随着更多 HTML5 API 和 WebGL 使网页上的图形更加先进,它使 voxel.js 项目成为可能,并创建了贡献者社区。
在我为 Intern.js 测试框架做出贡献的过程中,整体感觉是没有完全成熟的测试框架能够解决网络应用程序测试的所有需求。这个项目的目标是创建一个使用相同类型测试 API 的全能测试解决方案。
本书中所创建的框架侧重于使用现代技术,如 Web 组件,与流行的 JavaScript 库混合使用。在生态系统中,Web 组件领域尚未得到充分探索;因此,通过本书,我们旨在进一步拓宽网络开发者对这些技术的了解。除了扩展这些技能外,一个更大的目标是将框架开发过程变得更加易于接近,并揭开既定 JavaScript 系统的神秘面纱。
摘要
第一章开始,我们探索了网络应用程序开发过程是如何从纯基础转变为完整的软件平台的。我们研究了网络创新和挑战如何塑造本章讨论的框架,并在为网络开发者提供各种有用功能方面发挥巨大作用。在我的职业生涯旅程中,参与各种项目让我欣赏到通过结合优雅的模式和 JavaScript 编程语言的创造性使用所能取得的成就。
作为框架展示的一部分,很明显,生态系统在浏览器和其他 JavaScript 运行时功能的地方有很多选择。然而,在速度、功能和独特想法方面,总有改进的空间,这可以帮助我们提高开发过程。刺激这个生态系统的关键部分是不断发展的网络平台、ECMAScript 规范的制定,当然,还有 Node.js 和 Deno 等运行时维护者的辛勤工作。
在接下来的章节中,我们将更深入地探讨软件范式,重点关注框架组织和它们的架构模式。在第二章中,我们将探讨框架是如何构建和组织的。
第二章:框架组织
现有的 JavaScript 框架在技术和结构上有很多相似之处,这些相似之处对于成为框架开发专家来说是很有用的学习内容。在本章中,框架组织指的是将一组抽象和构建块组合起来的方式,从而创建一个可用的接口集合,这些接口可以在应用程序代码中使用。
我们将学习以下组织主题,这些主题有助于框架的开发和使用:
-
了解抽象
-
JavaScript 中抽象的构建块
-
框架构建块
-
区分模块、库和框架
理解框架开发的核心理念和方面将帮助我们构建自己的框架,并拥有使用其他框架的最大潜能的领域知识。软件框架的用户或利益相关者的期望是拥有清晰的指导、熟悉的应用概念、降低复杂性和一个定义良好的代码库。让我们探讨框架组织如何帮助我们满足这些期望。
技术要求
与上一章类似,我们将使用本书的仓库作为本章的扩展。您可以在 github.com/PacktPublishing/Building-Your-Own-JavaScript-Framework 找到它。对于仓库中的代码,您可以使用任何支持终端并运行 Node.js 的环境,例如 Windows、macOS 和大多数 Linux 版本。
本章包括开源框架的示例——为了节省空间,省略了不重要的细节,这些细节用 // ... 注释表示。您需要熟悉阅读 JavaScript 代码,但如果您不理解整个代码块也不要担心。在阅读代码的过程中,请确保跟随那些示例旁边的链接查看完整的实现,包括所有代码细节。chapter2/README.md 文件列出了本章可用的代码资源。
在本章中,建议尝试调试以进一步深化我们对框架结构的理解。在您的计算机上探索这一点的最简单方法是从 code.visualstudio.com 下载最新版本的 Visual Studio Code。
了解抽象
让我们深入框架组织的第一个方面——抽象的基本概念。软件开发框架在 Web 开发或其他领域的主要便利之一是向开发者提供高质量的、有见地的抽象。这意味着将可以跨越多行代码的任务,其中充满了实现陷阱,围绕它创建一个简单的接口。这也意味着提出一种智能的方法,将独立的接口结构化成熟悉、可扩展的、易于使用的模式。
这种将复杂性和对象泛化的概念抽象化的做法,帮助我们定义可以在我们的框架中用于多种目的的构建块。每个抽象对象都可以使用自定义属性集进行初始化,并在需要时以多种形状使用。在框架中拥有这种简化和泛化的好处,使得开发者能够专注于程序的商务逻辑。正是这些抽象概念通过消除复杂性、重复以及学习新系统的挑战,为开发者带来了好处。通过抽象,开发者不必使用甚至学习他们构建的系统中的低级组件。
计算机科学中的抽象
通常,抽象的概念,即某些复杂机制简化的表示,在软件开发中是至关重要的。这个概念在编程课程中很早就被教授,并且可以在大型和小型程序的高层和低层接口中实现。软件抽象结构了这些程序中的许多部分,并决定了程序的控制流程。定义数据如何表示的数据类型和结构可以被认为是建立在较低级对象实体之上的抽象。
一些编程语言提供了直接语法来编写抽象类和接口。TypeScript 作为其扩展 JavaScript 的一部分提供了这一功能。这允许开发者声明 抽象 类、方法和字段。你可以在 www.typescriptlang.org/docs/handbook/2/classes.html 找到一些优秀的示例,以供进一步阅读和原型设计。
如果我们看看使用纯 CSS、HTML 和 JavaScript 技术开发网站的过程,我们就可以发现许多预定义的抽象,使这个过程更容易访问和简化。例如,HTML 通过其元素与属性的组合,只需几行标记标签就能快速定义超链接和嵌入媒体。这些元素的样式通过一组针对特定元素节点的样式规则来定义。我们可以在 文档对象模型 API 中看到 Web API 抽象的例子,这是一个在复杂嵌套节点树之上的抽象,这些节点定义了文档结构。这些前端技术为用户在网页浏览器内的交互提供了一种方式,它泛化和简化了与 Web 应用交互的复杂性。
我们在这里可以看到一个简化的抽象层次结构,从开发者编写在最顶层的应用代码开始,到最低层的基本逻辑规则:

图 2.1:从高层到低层结构的抽象层次结构
这最终带我们来到了 JavaScript,作为一种高级编程语言,它已经在许多方面抽象掉了复杂性,例如内存管理、与浏览器的交互以及一般对象管理。事实上,一些编译为 JavaScript 的抽象甚至关注于抽象掉更高层次的组件。例如,GWT 这样的工具包和 Elm、C#、Dart 这样的编程语言通过编译为 CSS、HTML 和 JavaScript 来实现这一更高层次的抽象过程。ECMAScript 的语言扩展,如 TypeScript,在语法上更接近 JavaScript,抽象掉了我们在编写 JavaScript 程序时常见的陷阱,并通过添加编译步骤来提高整体开发者体验。
在第一章中,我们探讨了几个使用 TypeScript 的框架,这些框架依赖于另一个框架,或者两者兼而有之,以创建更高抽象级别的框架。例如,Nuxt.js 是一个 Vue.js 框架,它依赖于 TypeScript。在这种情况下,框架要求开发者使用这些语言扩展和它们自己定义的抽象来构建应用程序。当我们为 Web 平台和 JavaScript 生态系统开发时,思考抽象层次可以嵌套多深是非常有趣的。在前端,我们有网络浏览器,它管理网络请求/响应网络,绘制布局,启用交互,等等。后端应用程序服务在云服务器实例中的进程和操作系统基础设施之上运行。当我们放大到电线中的电流,它为我们应用程序代码提供所需的比特时,抽象层次会不断增长。
现在我们对抽象及其用途有了更多的了解,我们将探讨这种核心模式的缺点。
抽象的缺点
我们已经探讨了抽象的好处,但在利用或实现抽象时,也有一些缺点需要考虑。这些因素也适用于框架,并且对框架开发有重大影响。让我们讨论一下抽象可能导致走错路的一些方式:
-
抽象可能是不完整的——使用抽象覆盖底层技术的所有潜在用例可能很困难。例如,如果你有一个 Web 应用程序框架,这通常可能是一个无法支持以特定方式输出 HTML 的利基功能的情况。利基需求可能包括渲染不同类型的组件,例如 SVG 动画或直接 DOM 操作。框架提供逃生口来避免这些问题,但可能还有其他情况,我们必须依赖底层组件的知识,避免使用定义的抽象。同时,一个抽象可能错误地表示底层系统,这可能导致混淆或对底层概念的错误使用。例如,如果加密库错误地使用了原语,即使结果正确,也可能引入潜在的错误。
-
抽象引入了介于你和底层系统之间的额外代码层,可能会影响性能。在前端开发的情况下,这意味着需要通过网络传输更多的代码——额外的函数调用和间接层。在后端场景中,服务器实例使用更多的进程内存。性能也可能受到框架选择算法的影响。如今,框架作者和用户都非常重视性能,定期的比较和基准测试有助于处理这些缺点。
-
一些不同的框架抽象可能不会提供正确的接口或足够的控制给用户,这可能会限制系统的潜力。这可能是简单地不支持底层接口的所有方法。如果选择的抽象被用于其设计目的之外的事情,也可能发生这个问题。如果框架是在引入某种技术之前设计的,这也可能成为问题。例如,在某些框架中支持WebAssembly时,由于加载限制或必须使用外部组件,在某些情况下无法加载 WASM 模块。在一个缺乏框架的现有项目中引入和使用 WebAssembly 将是一种反模式。
特别地,在 JavaScript 生态系统中,模式和抽象思想变化很快。新的工具和解决方案出现,抽象了我们对前端交互的管理和后端服务的构建。这意味着,作为项目负责人,我们必须调整到变化中的平台,或者我们的现有抽象变得过时。这可能导致某些功能支持不足或代码普遍损坏。在许多情况下,当某些 Web API 随着 Web 平台引入新功能而改变或发展时,这种情况就会发生。
-
抽象的另一个缺点是,开发者可能知道如何使用某个框架创建应用程序,但对底层技术的内部结构一无所知。隐藏的复杂性可能导致在调试问题和追踪应用程序核心错误时遇到困难。不了解幕后技术也限制了开发者优化功能和利用高级功能的能力。
-
我们还可能面临所谓的“泄漏抽象”。这是指尝试完全隐藏某些系统复杂性的尝试并不成功。这通常会导致底层系统的细节暴露给抽象的使用者。这种现象可能导致代码更加复杂,并带来自己的问题。当开发者不得不深入研究底层系统的实现细节并尽力弄清楚抽象如何映射到底层系统时,问题变得明显。
-
在框架和一般情况下的高度主观抽象可能会在引入更复杂的层级时引发问题,因为它们强加了一些开发者可能不同意但无法更改的具体设计选择。这些选择可能会限制应用程序代码的可重用性和灵活性。如果我们看看 Next.js,它为其许多功能提供了高度主观的解决方案。例如,如果你计划在 Next.js 项目中添加 API 路由来构建 API,这些路由必须映射到
/api/端点。要了解更多信息,请查看Next.js 官方文档。这是一个简单的例子,但希望它能很好地说明这一缺点。
无论你在哪里引入抽象的使用,它都会给我们要与之交互的事物增加一个额外的复杂性和间接性层级。当我们通过各种方式添加抽象时,这使得我们的项目依赖于它们。这种依赖可能会产生某些复杂性。使用外部抽象时,我们必须接受使用它所带来的风险和权衡。
在下一节中,我们将探讨流行的抽象构建块,这些构建块通常用于框架开发,并作为框架的公共接口暴露出来。我们将深入研究前端浏览器 API 和后端运行时模块,以更好地理解框架利用什么来构建自己的抽象。这是一项有用的练习,因为它帮助我们了解这些框架的工作原理以及它们使用哪些技术将不同的工具组合在一起。这些追踪框架组织的练习对于成为框架领域的专家以及理解其背后的技术至关重要。
JavaScript 中抽象的构建块
在本节中,我们将讨论一些 JavaScript 中的详细抽象示例,以及 Web API 和作为框架中抽象构建块和基础组件的功能。框架和浏览器开发者投入了大量思考和辛勤工作来定义这些抽象,这些抽象使开发者能够真正巧妙地编写代码,构建有组织的代码,并创建出色的产品。
前端框架抽象
使用这些三种技术——HTML、CSS 和 JavaScript——它们使网站开发成为可能,我们得到了许多构建块,这些构建块已经抽象化了在网络上发布内容的挑战。然而,我们并没有得到一种特定、结构良好、有见地的构建复杂 Web 应用程序项目的方法。这正是前端框架主要填补了由网络核心技术提供的空白。在前端框架中,这两种情况下创建了抽象:
-
在现有的网络 API 之上,这些 API 内置在网页浏览器或 JavaScript 运行时中。
-
当从头开始基于框架的内部结构和有见地的定义构建新的抽象时。这些抽象的创新和独特方法使得特定的框架在开发者中变得受欢迎和喜爱。
浏览器引擎提供的以下 Web API 通常被前端框架抽象化:
-
文档对象模型(DOM)——这允许操作网页的结构。DOM 表示一个树,其中的节点约束对象。DOM API 提供了访问和修改这个逻辑树的能力。用户界面框架主要需要这个来显示渲染的视图和处理 DOM 交互和事件。即使是使用虚拟 DOM 的框架,也需要将它们的结构附加到真实文档上,以便在页面上可见。
-
document属性。浏览器还提供了复杂的 API,例如 WebAssembly API,它允许应用程序包含二进制代码模块。框架通常将这些底层模块的加载器作为其加载工作流程的一部分。 -
document.body.style.color = 'pink';。这个对象模型还提供了几个方法调用,如getComputedStyle();来获取有关对象样式的信息。 -
网络 API – 这些 API 具有使用 Fetch API 或 XMLHttpRequest API 进行异步网络请求的能力。框架利用这些 API 进行基本的网络操作,包括使用 GraphQL 结构复杂请求。网络 API 还提供了 WebSocket 功能。这些 API 提供了比常规网络调用更少的开销的全双工(数据可以同时发送和接收)通信,使得具有实时更新和通信的应用程序成为可能。WebSocket API 足够简单,可以直接在应用程序中使用或在特定框架中包含扩展包。Socket.io 建立在 WebSocket API 之上,提供了一个完整的低延迟解决方案,可以与框架代码共存。最后,WebRTC 也属于网络 API 的范畴,它允许在浏览器中捕获和流式传输音频和视频内容。与 WebSocket 类似,WebRTC 框架集成通常包含在外部库中,因为它是一个非常微妙的特性。
-
存储 API – 这些 API 具有存储数据以供 Web 应用程序和缓存使用的能力。这些 API 通常用于在本地和会话存储中存储数据。它们还写入浏览器 Cookie 和数据库,如 IndexedDB。例如,Angular 应用程序可以包含一个提供 Cookie 服务并使读取和写入 Cookie 信息更简单的依赖项。
-
后台服务 – 这些包括一系列服务,可以启用数据的后台同步、通知、推送消息等。Web workers 通常提供了一种运行后台独立脚本并利用多个 CPU 核心的方法。
-
图形 API – 这些 API 允许渲染高性能的 3D 和矢量图形。这包括 WebGL API 和 SVG 元素。3D 图形库使用 canvas 元素进行渲染,并可以利用图形硬件。对于使用 Vue.js 构建的应用程序,还有一个额外的组件库称为 VueGL,它使得创建基于 WebGL 的组件更加容易。至于 SVG,React 中的 JSX 能够直接解析 SVG 语法,只要将 SVG 属性转换为 camel-case JavaScript 语法即可。
可以在 developer.mozilla.org/docs/Web/API 找到框架可能利用的有用、深入的 Web API 列表。
现在我们来看一个使用 Nuxt.js 的真实世界前端示例。
真实世界的示例
Nuxt.js 使用 Vue 作为其前端组件的骨干。如图 图 2.2 所示,Nuxt.js 框架内置了 NuxtLink 组件用于创建链接,这些链接可以在应用代码中使用,并且利用了 Vue 的几个模块,例如 vue-router 和组件构建功能,如 defineComponent:

图 2.2:Nuxt.js 和 Vue 框架抽象
我们可以从应用代码中的example.vue页面详细跟踪这个抽象的使用(图 2.2):
pages/index.vue
<template>
<NuxtLink to="/">Index page</NuxtLink>
<NuxtLink href="https://www.packtpub.com/" target="_blank"">Packt</NuxtLink>
</template>
要在您的计算机上运行这个特定的代码示例,请导航到chapter2/nuxt-js-application目录并运行npm install && npm run dev。有关更多详细信息,请参阅包含的chapter2/README.md文档。一旦应用程序准备就绪,您应该在终端中看到可以打开的 URL。以下是一个成功输出的示例:
> npm run dev
> Local: http://localhost:3000/
> Network: http://192.168.1.206:3000/
框架的自动导入功能允许直接在模板文件中使用组件创建两个链接。这个内置组件在 Nuxt 框架的源代码中定义,位于github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/components/nuxt-link.ts。
让我们花点时间来理解nuxt-link组件代码,跟随框架本身的源代码。这个特定组件的代码扩展了 Vue.js 的路由行为。它定义了如NuxtLinkOptions和NuxtLinkProps这样的类型化 TypeScript 接口,以接受特定的样式属性和路由选项。defineNuxtLink函数返回具有定制路由行为的组件。如checkPropConflicts和resolveTrailingSlashBehavior这样的辅助函数处理特定的路由用例。setup()函数调用使用 Vue 3 组合 API 来启用组件的响应式属性并将组件生命周期钩子附加到应用中的NuxtLink版本。更多关于这个 API 的细节可以在 Vue.js 的文档中找到 – vuejs.org/guide/extras/composition-api-faq.html。这里以压缩形式展示了重要部分:
export type NuxtLinkProps = {
to?: string | RouteLocationRaw
href?: string | RouteLocationRaw
target?: '_blank' | '_parent' | '_self' | '_top' | (
string & {}) | null
// ...
}
export function defineNuxtLink (options: NuxtLinkOptions) {
const componentName = options.componentName || 'NuxtLink'
return defineComponent({...})
}
在前面的组件代码中,我们看到defineComponent的最终返回语句。这生成了我们在 Web 应用程序 HTML 结构最终源代码中看到的锚点<a>元素。它是由 Vue.js 内部函数调用生成的:
return h('a', { ref: el, href, rel, target})
从defineNuxtLink函数中也可以明显看出,可以修改组件的一些部分。例如,我们可以使用componentName参数定义一个具有自定义名称的组件。
从第一章,我们已经看到 JavaScript 为全栈框架提供了很多。在下一节中,我们将探讨可以作为后端环境一部分利用的 API。
后端运行时抽象
虽然有众多前端 API 可供选择,但我们仍然受限于网络浏览器支持的功能。当我们编写后端服务时,这是一个较小的问题,因为我们仍然可以使用许多 API,我们甚至可以编写自己的扩展或与外部系统集成以用于定制用例。让我们看看可以作为框架开发一部分使用的重要 API。
在本节中,我们将探讨Node.js和Deno,因为它们是提供类似功能的两个运行时。这些运行时需要处理服务器创建、文件和进程管理、模块打包等。以下是一些后端框架使用的必要 API:
-
文件系统 API – 这具有读取和写入文件以及其他文件系统实体的能力。框架在存储数据、加载现有文件和提供静态内容时,会大量使用这些 API。这些 API 还包括文件流和异步功能。
-
网络 – 这些是启动新的服务器进程和接受请求的 API。包括处理 HTTP 请求和其他请求格式。
-
模块和打包 – 这些是模块和包可以加载的约定。
-
操作系统 API – 这些是从运行进程的操作系统获取信息的 API。这包括有关内存消耗的有用数据和有用的操作系统目录。
-
进程处理 – 这些 API 允许操作和收集当前运行进程的详细信息。这些 API 还支持子进程创建和多进程通信的处理。
-
原生模块 – 原生模块 API 允许用户调用用其他原生语言编写的库,例如 C/C++、Rust 等。在某些情况下,它们使用外部函数接口(FFI)。WebAssembly 也是原生模块支持的一部分。
-
工作线程 API – 允许生成额外的工作线程,以便在主进程之外调度重负载服务器工作。例如,Deno 运行时支持Web Worker API以提供这些功能,而 Node.js 使用其工作线程模块。
-
控制台和调试 – 这组内部 API 允许记录进程日志。调试 API 使得开发和查找运行代码中的问题更加容易。与支持调试操作的编辑器配合使用,可以在框架请求处理器处理请求时暂停调试器。
这些是一些后端框架可以作为项目基础使用的 API。例如,hapi.js 框架能够结合一些这些 API 来创建其Server、Route、Request和Plugin模块。例如,其Core(hapi/lib/core.js)模块利用了操作系统、网络和模块处理 API。
接下来,我们可以看看 Nest.js 中抽象和运行时 API 组合的详细示例,这是一个我们熟悉的框架,见 第一章。
后端框架抽象
Nest.js 框架支持提供任何 HTTP 框架的能力,只要定义了与其一起工作的适配器。直接嵌入 Nest.js 的现有适配器是 platform-express 和 platform-fastify。HTTP 适配器抽象的默认行为对开发者来说是透明的,因为它默认使用 express 模块。
在 图 2**.3 中,我们可以看到所有组件的组合。应用程序代码由利用框架抽象和 Node.js API 的框架提供支持:

图 2.3:Nest.js 框架抽象
如 图 2**.3 所示,Nest.js 的 main.ts 入口文件启动服务器并监听传入的请求:
main.ts
const port = 5300;
const app = await NestFactory.create(AppModule);
await app.listen(port);
express-adapter,它从 AbstractHttpAdapter 扩展而来,定义了 HTTP 服务器所需的方法集,包括 .listen 方法:
import * as express from 'express';
import * as http from 'http';
import * as https from 'https';
// ...
export class ExpressAdapter extends AbstractHttpAdapter {
// ...
public listen(port: string | number, callback?: () =>
void): Server;
public listen(
port: string | number,
hostname: string,
callback?: () => void,
): Server;
public listen(port: any, ...args: any[]): Server {
return this.httpServer.listen(port, ...args);
}
// ...
}
上面的适配器代码利用了 express 框架和内部 http API。最终,它产生了一个公开设置 HTTP 服务器方法的类。尽管 express 提供了路由和 HTTP 辅助函数,但它本身并不启动服务器。在 express-adapter 中,有一个直接调用 Node.js API:
initHttpServer(options) {
const isHttpsEnabled = options && options.httpsOptions;
if (isHttpsEnabled) {
this.httpServer = https.createServer
(options.httpsOptions, this.getInstance());
} else {
this.httpServer = http.createServer(this.getInstance());
}
// ...
}
前一个代码块中的直接调用确定要启动的服务器类型,HTTP 或 HTTPS。它还接受各种 httpOptions 值。这种模式在其他框架中也很相似。例如,在 AdonisJS 中,框架作者定义了 HttpServer 类 (github.com/adonisjs/core/blob/master/src/Ignitor/HttpServer/index.ts),该类创建 HTTP 服务器并使用 createHttpServer 工具函数调用 Node.js 的运行时 API。
在进一步了解现有框架的结构和它们的抽象如何工作的时候,有一种方法可以遍历这些嵌套抽象的代码是非常重要的。在下一节中,我们将介绍调试技术,这可以帮助我们揭示框架中的隐藏接口。
关于调试
调试在软件开发中扮演着重要的角色。它帮助我们快速识别和解决问题。作为框架学习过程的一部分,它还帮助我们了解这些框架的内部工作方式。通过逐步执行程序的断点并深入调用堆栈,我们可以理解内部模块的内部工作原理。它还帮助我们穿越多个抽象层次。
Node.js 的调试器集成为我们提供了调试程序和框架的方法。自己尝试一下是一个好习惯,这样你可以更好地了解框架的工作方式。例如,要调试 Nest.js 应用,我们可以利用 Visual Studio Code 调试器:
-
在书的 GitHub 仓库的
framework-organization目录中打开nest-js-application项目。 -
运行
npm install以获取项目的依赖项。 -
在应用的
app.service.ts文件中设置代码执行断点;参考图 2.5中的截图。要设置断点,点击行号左侧的空白区域,直到你看到一个红色圆点。一旦出现红色圆点,那将是你的断点。 -
在 Visual Studio Code 中,浏览到
package.json文件,并按scripts部分。在图 2.4中查看此示例:

图 2.4:package.json 中的调试按钮
-
在按下
start:dev选项后,这应该会启动应用,但在nest命令中找不到可观察项,这意味着你需要使用npm install安装此项目的依赖项。 -
当应用以调试模式运行并使用
start:dev脚本时,在浏览器中打开http://127.0.0.1:3000地址。现在应该会在你的断点提取行上暂停。
如果你正确地触发了编辑器中的断点,这意味着你成功地将调试器附加到了应用上。现在你可以使用左侧的调用堆栈窗格(如图 2.5 所示)在运行进程周围导航并浏览 Nest.js 模块:

图 2.5:调试 Nest.js 应用
这种技术是快速了解框架幕后视图的一种方法。它使开发者能够快速了解框架的工作方式,并使理解嵌套抽象变得更加容易。要从中获得更多,你可以在code.visualstudio.com/docs/editor/debugging找到 Visual Studio Code 调试器的深入解释。
框架构建块
就像大多数编程语言一样,JavaScript 及其扩展,如 TypeScript,具有处理数字、字符串、布尔值、条件逻辑语句等基本功能。更高级的功能建立在这些基础之上。框架利用现有的接口,如事件和模块。然而,它们也通过定义接口来创建自己的构建块,以创建组件、路由等。
在本节中,我们将检查现有的接口和自定义接口。我们将查看一些常见的接口,这些接口可以组合起来形成一个框架。这些是抽象实体,它们解决了应用开发中的特定问题,并且对用户有益。
事件
事件绑定和事件在 JavaScript 应用程序中无处不在。它们通过按钮、表单、指针移动、键盘键、滚动等方式启用前端用户界面和交互性。事件绑定的概念是每个框架都通过不同的语法定义来处理的,如下所示:
// Vanilla JavaScript
someInput.addEventListener('keyup', keyDownHandler)
// Vanilla HTML
<input type="text" onkeyup="keyDownHandler()" />
// React
<input type="text" value={answer} onKeyPress=
{keyDownHandler}/>
// Angular key down combination of SHIFT + ESC keys
<input (keyup.shift.esc)="keyDownHandler($event)" />
// Vue key down
<input @keyup.shift.esc="keyDownHandler" />
大多数情况下,事件处理与原始 DOM 事件非常相似,但语法经过修改以更好地适应框架抽象。框架通过提供更复杂的事件管理组件来进一步启用事件处理。例如,Angular 有HostListener的概念(angular.io/api/core/HostListener),用于在其组件内注册事件。
在服务器端,Node.js 由于其异步、事件驱动的架构而高度基于事件,框架利用了这一点。例如,hapi.js 维护自己的事件发射器包,称为@hapi/podium,允许开发者注册自定义应用程序事件。
事件处理模式的另一个例子将是如何报告最新值的change和navigation事件:
board.on("ready", function() {
const gps = new five.GPS({
pins: {rx: 11, tx: 10}
});
gps.on("change", function() {
console.log(this.altitude);
});
gps.on("navigation", function() {
console.log(this.speed);
});
});
事件的使用是一个重要的构建块,它允许我们订阅用户交互并监听某些操作的更改或进度。既然我们决定开发自己的框架,它需要提供一种与事件交互并抽象它们周围某些复杂性的方法。
组件
许多框架提供抽象来创建可重用组件以组织项目。根据应用程序的计划,组件可以帮助将任何类型的应用程序拆分为可重用和独立的代码片段。这些代码部分也可以相互嵌套。根据所需业务逻辑,开发者可以定义自定义组件、使用预构建的组件或导入为特定用途设计的组件库。一旦许多组件嵌套并放置在一起,这些对象之间通常会有一些交互。组件利用数据属性从当前状态向用户渲染信息,在许多情况下,它们需要从父组件中获取某些属性。对于使用 React 或 Vue 的框架,这意味着编写通信模式,以实现子组件到父组件以及相反方向的通信。这个过程可能会变得复杂,这就是为什么这些框架使用单向或单程数据流,其中数据更新从父组件流向子组件组件。与其在嵌套组件之间同步相同的状态,不如建议在链中最常见的祖先组件中存储状态。
如果我们有一个复杂的应用程序,这意味着我们可能最终会有很多组件,嵌套多层。这就是 组件组合 可以帮助的地方。组件组合是一种允许最小化代码重复并提高性能的模式。
在下面的图中,我们有一个说明性的例子,说明了组合如何影响和重新组织应用程序内嵌套组件的集合。组件组织模式对开发者来说非常熟悉,因此使用或创建利用此模式的框架将是一个不错的选择:

图 2.6:嵌套与组合组件
生命周期方法
生命周期方法或事件通常由框架管理,提供在特定点执行代码的能力。这些方法可以用于在组件和其他系统的不同阶段执行自定义逻辑,这为框架接口提供了灵活性。这些生命周期方法可以在组件执行期间附加或分离额外的日志、实用函数等。生命周期序列,即这些事件发生的顺序,必须在框架中得到良好的文档和描述。这主要是因为生命周期方法可以遵循特定的命名约定,并且具有复杂的运行时层次结构。
在 Nest.js 中,服务器框架为其模块系统提供了生命周期钩子。其中的例子包括 onApplicationBootstrap(),它在所有模块在应用程序中初始化时被调用,以及 onModuleInit(),它在模块的依赖关系被解决时被调用。使用 Nest.js 中的 TypeScript 接口,我们可以在所有连接到服务器关闭时注入代码到 onApplicationShutdown 生命周期事件中,这可以定义如下:
@Injectable()
class SomeService implements OnApplicationShutdown {
onApplicationShutdown(processSignal: string) {
// ...
}
}
在 Vue.js 中,由于框架处理组件的渲染,可用的事件涵盖了组件的整个生命周期。例如,它有 beforeCreate、created 事件,组件初始化其状态时触发,以及 beforeMount、mounted 事件,组件被挂载到 DOM 树上时触发。你可以在 vuejs.org/guide/essentials/lifecycle.html#lifecycle-diagram 找到 Vue.js 的一个很好的生命周期图。
路由
前端和后端框架通常都需要某种形式的路由器,以导航到应用程序的不同部分。前端的路由机制遵循网页的导航模式,遵循浏览器的 URL 模式。在前端,路由器对于在状态之间转换或导航到内部或外部页面至关重要。除了提供路由树结构外,路由器还负责提供接口,允许组件调用路由行为——我们通过本章“实际示例”部分的NuxtLink示例看到了这一点。
react-router (reactrouter.com/main/start/overview)项目是路由组件所需一切的良好示例。它使得在组件内简单地定义路由成为可能,如下所示:
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Login />} />
<Route path="register" element={<Register />} />
</Route>
</Routes>
后端服务器框架使用服务器路由来处理发送到 API 端点的请求。通常,路由接口采用不同的端点 URL 结构形式,并将这些映射到处理路由的函数。在 Express.js(expressjs.com/guide/routing.html)中可以找到一些无意见的路由器的良好示例。
从以下代码示例中,我们看到端点路径与能够处理请求并返回文本响应的函数之间的关系:
app.get('/framework-organization', (req, res) => {
res.send('Learn about framework organization!')
})
从第一章中,我们看到了基于文件的路由示例,这通过仅查看应用程序中的文件来进一步简化路由机制,并基于这些文件动态创建路由。
模板引擎
另一个关键构建块是模板引擎。这个引擎将标记文档的静态部分与来自应用程序的数据结合起来。模板使得能够以各种形式的数据渲染视图。使用前端框架,这通常意味着渲染嵌套的组件层次结构。模板引擎的任务是启用数据绑定,并将任何指定的交互式组件(如按钮或输入字段)的事件绑定。
使用后端框架,模板引擎渲染整个页面,或者在某些情况下渲染部分页面,通过网络发送到客户端进行初始静态渲染。从第一章中,我们看到了像 Next.js 这样的框架,它能够在服务器端渲染前端组件,然后将任何 JavaScript 行为附加到已渲染的组件上。默认情况下,在 Next.js 中,页面是预先渲染的,以提高搜索引擎优化和浏览器客户端的性能。模板是框架的一个关键构建块——这是开发者创建表示层和标记页面结构的方式。
网络连接
Web 框架通常提供几个组件作为网络抽象。使用良好的网络接口可以极大地提高应用程序的可读性、性能和错误处理能力。以下是一些通常作为良好网络抽象一部分的功能:
-
会话管理 – 这是指管理会话并提供对会话信息的便捷访问的能力。这作为网络的一部分被包括在内,因为前端框架通常依赖于后端服务来获取和解析会话信息。
-
错误处理 – 这提供了良好的接口来处理在向端点发出请求过程中可能发生的所有类型的错误。
-
缓存 – 这是提供缓存层以改善性能并避免在数据已经足够新鲜的情况下进行冗余查询的机制。
-
安全性 – 通常,框架内置了遵循最佳实践的安全功能。这包括例如 XSS、CSRF 保护、脚本注入预防以及输入验证的示例。
-
请求和响应管理 – 这提高了使用所需参数发出请求并解析来自外部系统的响应的能力。
大多数这些网络抽象都适用于前端和后端系统。在全栈框架中,这些抽象的组合可以极大地提高所支持系统的作业流程和效率。
所有这些抽象都是在 JavaScript、TypeScript 中实现或由运行时启用的。它们的实现可以来自三类代码结构 – 作为模块、库或内置于框架中。在下一节中,我们将探讨这些代码组织类别。
区分模块、库和框架
在开发 JavaScript 应用程序时,我们依赖于模块、库,当然还有更大的框架。这些结构可能来自内部和外部来源,这意味着它们要么是你或你的团队编写的,要么是其他人编写的依赖项。特别是 JavaScript,它处于一个独特的位置,其中模块、库甚至框架都可以在浏览器和服务器环境中使用。对于框架开发者来说,了解如何与这些 JavaScript 结构协同工作非常重要,因为框架在很大程度上依赖于定义和使用模块和库。这些抽象和结构允许更好的代码组织,我们将在下一节中讨论这一点。
模块
开发者创建自己的模块以将代码分割成多个文件或逻辑块。以类似的方式,模块也可以从外部来源导入。模块封装将代码块包裹起来,提供了一种通过字符串、函数和其他数据类型导出各种数据类型的方法。
JavaScript 中模块的定义和使用历史相当复杂;它探索了不同的模块模式和实现。即使在现代项目中,你也会发现模块管理的不一致方法。最初,没有组织模块的方法,因此前端 JavaScript 模块被封装在立即执行的函数表达式或对象中。使用函数可以在词法作用域内托管其内部的所有值。以下是一个示例:
let myModule = (function () {
// ...
return {
someProperty: function () {
// ...
}
}
})();
显然,这种语言需要某种模块模式,这就是 CommonJS 和 AMD 类型模块被引入的地方。定义 CommonJS 模块很简单,可以在许多 Node.js 框架中看到其使用情况:
module.exports = class MyModule {
constructor(someProperty) {
this.someProperty = someProperty;
}
myMethod() {
return this.someProperty;
}
};
例如,hapi.js 在其大多数文件中使用了类似的 CommonJS 模块模式,这些模式可以在框架仓库 github.com/hapijs/hapi/tree/master/lib 中找到——其模块列表中的一个简单示例是 lib/compression.js 文件,位于 lib 目录下:
const Zlib = require('zlib');
const Accept = require('@hapi/accept');
exports = module.exports = internals.Compression = class {
// ...
accept(request) {
const header = request.headers['accept-encoding'];
// ...
}
};
此模块为许多 hapi.js 用例提供了压缩功能。省略了一些代码后,我们看到 exports 关键字,它被用来使此模块的方法在其他文件中可用。
这些天,项目可能包含不同类型的 JavaScript 模块,这些模块作为其工作流程的一部分被使用。你将看到的更标准的类型是 CommonJS 和 module.exports 关键字。ESM 系统提供了 import 和 export 关键字来管理模块。为了区分模块类型,.cjs 和 .mjs 文件扩展名被用来明确指出使用了哪个模块系统。正常的 .js 扩展名仍然可以使用,但这时模块加载系统需要确定如何加载这些文件。
在生态系统中你可能看到的模块类型的一些示例列在这里:
-
通用模块定义(UMD)—— 这是尝试支持所有可能模块声明的模块定义
-
require()和define()函数用于管理模块 -
立即执行的函数表达式(IIFE)—— 这些是由函数作用域封装的简单模块
模块系统在 JavaScript 中正在逐渐变得更好,但在使用框架中的各种模块和选择合适的模块系统时,这是一件需要注意的事情。模块的不同使用可能导致特定环境中的加载问题,或者导致某些功能无法按预期工作。
库
在当今的软件开发中,库是不可或缺的;它们在支持任何规模的网络开发项目中都发挥着重要作用。这些库由一组实现特定功能且接口定义良好的资源组成。库的焦点是包含封装的优化功能以解决某些问题。大多数库都试图专注于一组特定的问题,以帮助他们的利益相关者。JavaScript 拥有丰富的开源库,开发者在专业项目中无法离开它们。它们不指定任何特定的意见控制流程,而是让开发者在他们需要时使用它们。与框架类似,JavaScript 运行时的技术可用性允许一些库在浏览器和后端环境中使用。
库可以作为框架用于解决某些技术挑战的核心组件。我们看到了许多框架围绕以下库构建抽象的例子:
-
lodash (lodash.com) – 这个库提供了一组用于常见任务的实用函数
-
React (reactjs.org) – 这是一个极其流行的用户界面组件渲染库,具有状态管理功能,许多前面提到的前端框架都是基于它构建的
-
Axios (axios-http.com) – 这是一个强大的前端和后端 JavaScript 项目的 HTTP 客户端库
-
Luxon (moment.github.io/luxon) – 这是一个用于在 JavaScript 中操作日期和时间的库,是流行的 moment.js 库的演变
-
jQuery (jquery.com) – 这是一个超过十年的流行库,它简化了 DOM 遍历,并抽象了 CSS、AJAX 等跨浏览器的怪癖
-
Three.js (threejs.org) – 这是一个 JavaScript 3D 库,它抽象了 WebGL 和 Web 上的 3D 图形的复杂性
在现有库的基础上构建额外的工具以支持各种类型的应用程序是一种常见的模式。例如,在第一章中,我们看到了一些框架围绕 React 构建工具和抽象的例子。如果项目允许,通常利用库或从现有实现中学习已解决的问题,而不是重新构建或重写相同的代码,这是一个好主意。在下一节中,我们将比较选择一组库的开发工作流程与框架驱动的流程。
框架
从第一章,我们已经知道 JavaScript 框架的作用以及它们提供的优势。了解框架对其工作流程和功能的依赖程度,以及它们之间的差异也同样重要。库和框架都管理应用程序中的控制流。这种控制流是应用程序中逻辑流动的顺序和结构。使用库工作流程时,现有程序将有自己的控制流,作为其中的一部分,当需要时将执行库函数。现有程序精确地调用库中的可重用代码。这给了开发者完全的控制权,让他们能够根据需要结构化应用程序,从而为微调应用程序行为提供更多空间,但同时也错过了使用框架可能获得的潜在价值和结构。
使用框架工作流程时,框架决定了控制流应该如何结构化。在这种情况下,开发者需要在框架的限制内工作,并遵循由他人定义的通常严格的指南。
一个好的比较是 React 库与依赖于该库的 JavaScript 框架,如 Next.js。库仅由执行特定任务的函数组成。React 包含渲染、创建组件和其他方法的逻辑。但定义应用程序架构的是 Next.js – 框架 – 它在内部使用库方法来启用其功能。
在框架内使用库的工作流程选择使其成为一种强大的组合;这样,就有可能获得这两种工具的好处。
框架组织展示
您可以尝试本书库中的框架组织示例。通过从 github.com/PacktPublishing/Building-Your-Own-JavaScript-Framework 克隆仓库来访问示例。然后使用您的终端将目录更改为仓库中的 framework-organization 目录,并运行 npm install 后跟 npm start。遵循终端中的指导,并留意目录中的 README.md 文件以获取更多信息。
摘要
我们已经探讨了抽象与巧妙的 API 设计的结合,这共同构成了构建成功框架的关键。我们还扩展了我们关于为开发者提供价值并使应用程序开发过程更加高效和易于接近的常见框架接口的知识。了解模块、库和框架的使用方法有助于我们成为更好的系统架构师。此外,使用调试器快速探索现有框架中所有这些组件如何组合在一起的能力,使我们能够成为更加高效的开发者。
现在我们对各种组织模式更加熟悉了,我们可以更深入地探讨那些帮助我们构建新系统的具体技术。在下一章中,我们将探讨现有的和常见的模式,这些模式将我们所学的基础构件组合成一个统一的系统。
第三章:内部框架架构
在前面的章节中,我们学习了当前框架的历史,探讨了抽象的概念。我们还研究了 JavaScript 框架如何使用、组合和扩展不同的抽象来使框架功能化。在本章中,我们将深入研究 JavaScript 框架的架构模式。为了更进一步并扩展我们的框架知识,我们必须仔细检查现代框架的构建内容以及我们可以从现有模式中学到什么。
作为本章的一部分,我们将探讨以下内容:
-
理解现有前端和后端框架的核心技术架构模式。我们将关注结合成一个单一系统的设计、架构和结构模式。
-
概览框架 API、打包配置和附加工具。
-
理解对框架有益的附加工具。
-
了解可用的编译器和打包器。
技术要求
与上一章类似,我们将使用本书的存储库作为本章的扩展。您可以在github.com/PacktPublishing/Building-Your-Own-JavaScript-Framework找到它,相关文件位于chapter3目录中。要运行该存储库中的代码,您可以使用任何支持使用终端或命令提示符、运行 Node.js 的环境,例如 Windows、macOS 和大多数 Linux 版本。
本章中提到的大部分代码都可以在本书的存储库中找到,因此您无需导航外部存储库。请按照chapter3目录中的README.md说明开始。此外,许多框架架构都关注于将使用该框架构建新项目的利益相关者或开发者。在本章中,我们将称他们为框架用户,以免与使用这些框架构建的应用程序最终用户混淆。
探索核心架构
让我们探索构建框架的核心架构组件。当一个框架项目被创建时,它通常被划分为一个有组织的目录结构,包含各种专业部分。这种方法有助于分离特定模块、脚本和文件的关注点。这种组织模式类似于网络应用项目的组织方式。但在框架的情况下,项目需要导出公共接口和脚本以便使用。在某些情况下,框架也可以拆分为多个存储库,以允许采用不同的框架开发方法。
由于每种语言的编程环境都不同,JavaScript 和 TypeScript 框架都有自己的方式来构建框架项目,这使得生成工件和使框架在利用它们的项目中更容易使用。一个组织良好的项目使得维护、协作和重构该项目的许多部分变得更加容易。例如,让我们以 Vue.js 和 Angular 这样的现实世界示例为例。Angular 将大多数框架文件保存在单个存储库中(可在 github.com/angular/angular 找到),除了其命令行工具(位于 github.com/angular/angular-cli)。然而,许多包作为独立的依赖项发布在包注册表中,例如 npm(可在 npmjs.com/package/@angular/core 找到)。框架包的结果可能与前端框架不同。在 Angular 的情况下,它由针对不同版本的 JavaScript 的预构建文件组成。
在 图 3.1 中,Angular 核心以未扁平化和扁平化版本提供,位于 esm2020、fesm2015、fesm2020 目录中,这些包还包含了各种源映射:

图 3.1:作为 npm 包提供的 Angular 核心文件
根据目标浏览器或使用的打包技术,Angular 提供了多种导入选项。Vue.js 也有类似的打包导出,提供了多种选项来在特定的 JavaScript 模块环境中加载框架。
图 3.2 中的输出目录是使用 rollup.js (rollupjs.org) 打包 Vue.js 框架的结果,为每个目标运行时创建输出配置。该配置的源代码可以在 github.com/vuejs/core/blob/main/rollup.config.js 找到:

图 3.2:Vue.js 作为 NPM 包的分布
针对配置产生的文件会随着运行时目标的变化而缓慢演变。例如,如果框架通常不作为全局变量包含在 <script> 标签内,那么删除全局变量输出或将框架用户的输出转换为满足其需求可能是有意义的。
打包框架的示例
您可以通过在 chapter3 目录中运行 npm start 脚本来查看框架打包的示例。一旦打包的框架源代码下载并解压,您就可以查看 Angular 和 Vue.js 的几个输出结果。
现在构建步骤在 JavaScript 项目中如此普遍,这些框架也提供了直接导入框架包并将其包含在构建过程中的方法。框架的“未扁平化”版本就是这种情况的一个例子,其中框架文件没有被连接。相反,打包器会合并这些文件,并在可能的情况下使用代码拆分等技术进行优化。
使用框架的应用程序在package.json文件中导入依赖项。Angular 将框架的不同部分拆分为不同的包:
"dependencies": {
"@angular/animations": "¹⁵.2.0",
"@angular/common": "¹⁵.2.0",
"@angular/compiler": "¹⁵.2.0",
"@angular/core": "¹⁵.2.0",
"@angular/forms": "¹⁵.2.0",
"@angular/platform-browser": "¹⁵.2.0",
"@angular/router": "¹⁵.2.0",
},
将这些模块拆分为各自的包,为不同的模块之间创建了一个明确的边界。然而,通常需要额外的发布工具来简化管理和发布多个模块,而无需手动打包。拆分的包也使框架用户受益,因为他们可以根据自己的应用程序选择所需的模块。
Angular 核心依赖项的实际应用
第一章([B19014_01.xhtml#_idTextAnchor015])中的框架展示已经包含了本节中提到的所有 Angular 依赖项的现有示例。你可以在chapter1/angular目录中找到应用程序,运行它,并根据你的喜好进行调整。许多这些核心依赖项可以在npmjs.com/search?q=%40angular的npm包列表中的@angular命名空间中找到。
在 Vue.js 的情况下,核心架构被分散在许多 GitHub 仓库github.com/vuejs中。核心、路由、开发者工具、文档和其他框架部分被分散在 Vue.js 组织的不同仓库中。在构建新的框架时,通常更容易在一个仓库中管理大部分内容,甚至将所有内容都放在一个包中。结构仍然可以很好地分离,并且可以避免管理额外仓库的额外摩擦。随着项目的增长,你可以根据自己的需要将其拆分为多个仓库。到那时,框架应该已经建立了一个稳定的发布模型和基础设施来支持这种扩展。
为了更好地理解框架设计和维护的更广泛的架构模式,我们将探讨构成框架技术架构的三个重要部分:
-
模块:代码库的独立部分,通常位于单个 JavaScript 文件中。这些模块通常导入或导出函数、类或其他类型的结构,这些结构随后作为更大系统的一部分被使用。
-
包:框架背后的主要源代码包括暴露给用户的接口和用于内部实现某些功能的接口。
-
脚本:作为框架一部分公开的二进制文件和脚本。其中一些脚本也用于框架开发和特定用例所需的额外工具。
-
编译器:作为框架的一部分包含的程序,这些程序要么从框架中生成主要的 JavaScript 输出,要么作为开发过程的一部分使用。
并非这些类别中的每一项都是成功框架所必需的,但作为作者,我们可以挑选出对我们项目重要的部分,并专注于这些部分。如果我们观察现有的框架,我们会看到适用于 JavaScript、前端和后端项目的相似模式——这些项目采用了所有或大部分这些建筑类别。
在下一节中,我们将探讨框架开发中的两种技术模式——这些包括影响不同框架如何工作的架构和设计决策。
模式
专注于框架开发需要了解不同类型的软件模式,例如架构、设计和技术模式。了解这些模式以及它们在现有框架中实现时所做出的决策,可以帮助新的框架作者在他们的项目中取得成功。
建筑模式
我们在第一章中看到了现有建筑模式的例子,如 MVC 和 MVVM。JavaScript 框架有自由选择任何类型的架构模型,只要它们认为适合它们的用例。例如,基于组件的架构在现代前端框架中非常相关,并被许多系统使用,特别是那些扩展 React 库以增加其功能集的系统。在这种模式中,每个组件都封装了自己的状态、视图和特定的行为,甚至可能包含嵌套组件。
在后端框架中,中间件架构模式通常被用来管理或修改传入的请求和传出的响应。这种模式由于服务器请求和响应的性质而非常有效。
在 Electron.js 应用程序框架中,我们可以看到对建筑模式的不同方法。大部分情况下,Electron.js 是在界面渲染进程和主进程之间的进程通信之上设计的,用于低级操作。这种架构方法虽然没有定义特定的名称,但仍然将架构引导到关注点的分离。如果你查看一些 Electron.js 应用程序的代码库,你会注意到界面和后端操作之间的组织。框架也可以使用结合面向对象、函数式和响应式编程方面的混合架构模式,以实现最大的灵活性。许多这些概念可以在 Nest.js 框架中看到,如第一章中所述。
通过浏览各种开源框架的设计决策和代码,你可以找到不同架构模式的多种实现。作为一个未来的框架作者,我鼓励你在这一领域进行创新,通过创建你自己的原始架构模式或从既定模式中推导出你自己的方法。
设计模式
在设计模式方面,这些模式与架构模式相比处于更低的层次。这些模式解决的是框架如何解决组织代码和技术以实现一致架构的常见挑战。
在第一章中,我们通过观察者模式看到了一个设计模式的例子。除了观察者技术之外,框架还可以利用工厂模式,这有助于根据某些定义创建和管理可重用对象。根据实现和环境的不同,额外的增强可能包括管理创建的对象。在所有类型的 JavaScript 框架中经常看到的设计模式还包括发布和订阅模式。这种模式允许框架内部和基于框架抽象构建的组件通过发出事件和订阅这些事件来相互交互,从而在系统不同部分之间创建一种异步通信方式。
模块和模块设计模式在所有 JavaScript 软件中也同样普遍。你将发现由语言本身定义的模块 API 以及由不同框架精炼的模块抽象。这种模式主要集中于强制封装、标准化代码库以及防止大量复杂的代码块。
在下一节中,我们将探讨技术架构。它包括与技术方法相关的细节,例如概述 API、定义入口点以及使用额外的工具来启用框架行为。
技术架构
技术架构和模式主要处理技术挑战。对于 JavaScript 应用程序来说,这可能意味着处理页面渲染、响应请求、与数据库交互、加载应用程序等等。在框架中,技术挑战不仅限于解决特定技术问题。相反,它关于创建一个精心设计的包装系统,这对框架用户构建项目是有益的。
为了创建这个系统,框架作者需要结合一组包装接口和一组可用的脚本,并使用额外的软件来改善 JavaScript 编程体验。
在学习技术架构的过程中,我们将探讨三个类别,这些类别使得框架的基本功能得以实现。我们将探索这些技术主题下的所有子类别,正如在图 3中所示。3*:
| 包 | 脚本 | 编译器 |
|---|---|---|
| 核心 API | 二进制和可执行文件 | 框架编译器和打包器 |
| 入口点 | 文件生成器 | 类型 |
| 开发者工具 | 源映射 | |
| 插件和扩展 API |
图 3.3:技术架构的子类别
我们将从框架的核心包开始,这些包能够提供强大的功能。
包
包包含框架的核心逻辑,以及公共和私有 API。此目录可以包括框架运行所必需的任何包。它包括内部和公共接口。它还可以包括编译器或任何作为框架开发或开发者用例一部分使用的构建工具。这些是构成框架核心的部分。根据框架的不同,包可以是功能独立的,也可以相互依赖以实现功能。通常,框架的包部分可以包括任何应作为框架一部分包含的相关代码,但今天框架包括的包类型有几种基本类型。我们将在本节中更详细地探讨这些内容。
核心 API
根据框架的不同,“核心”包可能包括各种模块,用于提供公共用户暴露的接口和私有接口以启用框架功能。在第二章中,我们看到了框架包提供的公共 API 的示例,例如路由、事件管理、模板模块等。核心包在前后端框架中通常结构相似。
前端框架启用@vue/reactivity(npmjs.com/package/@vue/reactivity)。响应性是框架功能集的重要组成部分,我们将在后面的章节中进一步探讨它。
依赖注入(DI)是某些框架的核心,允许框架内和应用程序内的模块声明它们的依赖关系并使用外部接口。作为 DI 的一部分,框架也有接口来声明依赖提供者。你将在一些框架中找到 DI 的使用,最著名的是 Angular。有关 Angular 的 DI 的指南和更多详细信息,请参阅angular.io/guide/dependency-injection。由于 JavaScript 缺乏接口和类型,DI 功能并不像 TypeScript 或其他编译器那样受欢迎,这些功能是通过 TypeScript 或其他编译器启用的。你也会在 Nest.js 的后端找到注入功能。
你将在 Angular 和 Nest.js 中找到熟悉的 DI 模式(如下面的代码块所示);@Injectable装饰器的实现定义在框架的核心包之一,在github.com/angular/angular/blob/main/packages/core/src/di/injectable.ts:
// service: book.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: ‹root›
})
export class BookService {
constructor() { }
}
// module: book.module.ts
import { BookService } from './book.service';
@NgModule({
providers: [BookService],
})
export class BookModule {}
另一个常见的核心 API 是 createRenderer() API,用于执行上述操作——你可以在 vuejs.org/api/custom-renderer.html 上了解更多信息。
全栈和后端框架提供了一个 @sveltejs/adapter-node 适配器。此外,框架提供了几个官方适配器,具体请参阅 kit.svelte.dev/docs/adapters,这些适配器允许你为最大灵活性和与部署环境的兼容性定义适配器。也可能出现这样的情况,即前端框架仅将其服务器作为其内部组件的一部分来启用开发功能,但在生产用例中,开发者需要提供自己的服务器。
Svelte.js node.js 适配器的来源
在安装脚本提取之后,chapter3 目录提供了 Svelte.js node.js 适配器的源代码。框架使用这个源代码以框架友好的方式消费代码输入并生成输出。
通常,所有类型的框架都有一个 共享包,可能包含公共和私有 API。通常,最通用的逻辑,在许多包中重复使用,会放入共享包中。共享包的竞争者可能包括与运行时环境和基础相关的代码。例如,这个包可以包括实用函数、管理并转义 HTML 实体、处理 HTTP 状态码、在整个框架中规范化通用值,以及存储框架中可用的常量值。
在下一节中,我们将探讨框架的入口点——即连接核心 API 与框架执行开始的粘合剂。
入口点
框架用户与框架交互的主要方式是通过一个 入口点。这个定义与简单程序的入口点非常相似,并且是我们运行简单计算机程序时执行开始的地方。根据框架的抽象和结构,这种入口点可能会有很大差异。
在前端 JavaScript 框架中,入口点的概念有所不同。这些框架可以通过 <script> 标签包含,然后通过调用入口点在页面上进行初始化。例如,Angular 有一个根引导模块,用于初始化应用程序:
app.module.ts
@NgModule({
imports: [
// ...
],
declarations: [
AppComponent,
],
bootstrap: [
AppComponent
]
})
export class AppModule { }
在前面的代码中,AppModule 根模块对于初始化应用程序至关重要,因为它在浏览器中加载。它还定义了包含顶层导入和服务提供者的空间,以在应用程序中启用外部功能。
Ember.js 通过定义一个 Application 类的实例来实例化一个新的应用程序,使用类似的模式。这个 Application 类扩展了 Ember.Application 类,并提供了带有配置选项的对象字面量。此对象用于配置应用程序的各种组件和功能。此入口点类用于在开发人员进一步扩展项目功能时持有应用程序的其他类。要获取关于此 Application 类的精确细节,请查看其 API 文档,位于 api.emberjs.com/ember/release/classes/Application。
将 SvelteKit 视为一个不同的框架,由于它定义抽象的方式,它依赖于编译器和构建工具作为初始入口点。编译器检测根目录中的主 page.svelte 文件,并将其视为应用程序索引页的入口点。与 Angular 和 Ember.js 不同,这是一个更加简洁的入口点。
另一个入口点示例是 Nuxt.js 中的配置文件:
nuxt.config.js
export default defineNuxtConfig({
// My Nuxt config
})
此文件定义在项目根目录中。它允许进行框架配置和扩展,并接受各种选项。所有这些可能的选项都可以在 nuxt.com/docs/api/configuration/nuxt-config 中找到。
在纯后端框架方面,通常入口点是启动服务器的引导文件。随着服务器进程启动,它初始化服务器配置,例如正确地将进程绑定到特定端口。这个过程在 AdonisJs 的 docs.adonisjs.com/guides/application#boot-lifecycle 中得到了很好的说明,它作为一个状态转换机。这个引导过程也与 NestJs 类似,因为框架有一个用于引导服务器的服务器端 AppModule。
以下源代码是用于初始化应用程序的 Nest.js 引导脚本。在这个过程中,必须导入 NestFactory 和 AppModule。await listen 允许监听传入的请求:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const application = await NestFactory.create(AppModule);
await application.listen(process.env.PORT ?
parseInt(process.env.PORT) : 8080);
}
bootstrap();
此入口点文件可以使用环境变量在所需的端口上引导应用程序。包含的 AppModule 文件包含用户定义的附加模块,这些模块将在脚本启动时加载。这种模式在其他框架中也很普遍,例如 Express.js 和 Hapi.js。
开发者工具
在开发应用程序时,您可能会发现一些开发者工具集,这些工具可以使得与框架交互更加容易。这些工具旨在帮助进行性能分析、调试和其他任务。在前端项目中,这些工具通常以浏览器扩展或独立应用程序的形式提供。
Vue.js 通过浏览器和独立工具作为其工作流程的一部分提供了一些集成良好的工具。它允许我们快速了解应用程序结构,并进一步调试应用程序的输出。图 3.4显示了这些工具在行动中的示例:

图 3.4:Vue.js 浏览器开发者工具
前端框架提供了一些开发者工具的好例子,大多数是作为浏览器扩展,有时作为独立应用程序来解耦浏览器工作流程:
-
Angular DevTools (angular.io/guide/devtools): 这些工具提供了性能分析能力和调试功能。它渲染组件树,类似于 Vue.js 中图 3.4所示的内容。
-
Vue.js DevTools (github.com/vuejs/devtools): 如图 3.4所示,它提供了一个详细的组件树,带有搜索功能,以及一个路由列表,有助于调试路由配置。它还启用了时间线视图,以显示与 Vue.js 应用程序的交互历史。
-
Ember 检查器 (guides.emberjs.com/release/ember-inspector): 这套工具提供了大量的功能。它提供了一种探索 Ember 对象、组件树、路由、数据视图等功能的方式。
-
React 开发者工具 (beta.reactjs.org/learn/react-developer-tools): 这些工具允许您检查组件,编辑组件的实时属性,并修改状态。
-
SolidJS 开发者工具 (github.com/thetarnav/solid-devtools): 这些工具可视化和增加了与 SolidJS 响应式图交互的能力。与其他工具一样,它可以检查组件状态并导航树。
在您的框架中支持开发者工具时,可以考虑的一个有趣挑战是跟上框架的更新。Vue.js DevTools 通过针对框架的主要版本来解决这个问题——它为每个版本定义了一个包,例如app-backend-vue1、app-backend-vue2、app-backend-vue3等等。鉴于这些通常是浏览器扩展,它们在利用DevTool扩展浏览器 API 方面具有类似的架构(developer.chrome.com/docs/extensions/mv3/devtools)。
您可能会注意到后端框架缺少额外的开发者工具。在这些情况下,开发者依赖于调试器,如文本编辑器或 IDE,作为他们选择的工具。我们在调试 Nest.js 应用程序时,研究了第二章中的 Node.js 应用程序和框架调试。
在框架开发中,最初引入额外的特定开发者工具不是必要的一步。然而,这使得框架的使用变得更加愉快,并赋予了用户更多的能力。如今,JavaScript 运行时环境通常拥有出色的工具,无论使用哪个框架,都能帮助开发。
插件和扩展 API
在许多情况下,框架提供的包也支持可扩展性。这种可扩展性使插件和扩展开发成为可能,以多种方式使框架受益。提供的 API 使其他开发者能够自定义功能,并添加针对应用程序特定需求的新功能。它还允许框架专注于交付主要功能集,而不在框架的核心中包含每个潜在的功能。
这种可扩展性可能作为内部开发的一部分很有用,框架作者可以根据扩展接口创建适配器和接口。它也可以用于外部用例,其中核心开发团队外的开发者可以为特定用例创建功能,这些功能可以插入到框架中。让我们看看我们在 第一章 中讨论的框架提供的某些插件接口示例:
-
Bootstrap (
getbootstrap.com/docs/5.0/extend/approach/) 的文档包含创建可定制组件的指导原则,这些组件与核心功能配合良好。对于这个项目,开发者定义了一套规则作为指导方针。 -
ember install <addon>。它的文档中有一个完整的部分,支持插件开发。例如,大多数应用程序必需的功能,如身份验证,通过Ember Simple Auth插件 (ember-simple-auth.com) 提供。 -
执行插件逻辑的
install函数。 -
使用
ng命令行工具,开发者可以快速生成这些新库。作为库工作流程的一部分,新模块会被发布到 npm 以供他人使用。 -
Gatsby (gatsbyjs.com/docs/plugins) 有几个插件和非常好的文档,这有助于它们的开发。它提供了开发通用插件的工作流程,适用于任何用途,特定于单个项目的本地插件,以及转换插件,用于在类型之间转换数据。
-
SolidJS (solidjs.com/ecosystem) 定义了一个适用于不同目的的插件生态系统。开发者可以从各种 UI、路由、数据和通用插件插件类别中选择。最受欢迎的类别是用户界面添加,这使得接近各种网络应用布局和小部件变得更加容易。
-
Svelte (sveltesociety.dev/tools) 拥有一套旨在提高打包、调试和编辑体验的工具集。
-
Hapi.js (hapi.dev/tutorials/plugins) 提供了一个强大的接口来扩展框架的服务端功能。插件具有相当简单的接口,其中使用异步函数来注册和执行任何额外的扩展功能。
-
AdonisJS (packages.adonisjs.com) 提供了扩展功能的“包”。为了加快包的开发速度,Adonis 使用了 MRM 代码修改预设,可以在 github.com/adonisjs/mrm-preset 找到。它允许你快速为其框架搭建包的脚手架。
从我们刚才讨论的例子中,你可能会希望看到,在编写你的框架时,启用这种类型的可扩展性是很好的。这将有助于框架的成长,并使所有参与你框架生态系统的人受益。
在下一节中,我们将探讨各种脚本,这些脚本有助于管理当今的框架。
脚本
每个框架都需要能够代表其用户执行任务,对于构建框架的人来说也是如此,这就是架构需要引入各种脚本和程序来执行日常任务的地方。这些脚本通常有助于开发者提高效率并消除重复性任务。定义良好的强大脚本也可以使框架非常易于使用。在本节中,我们将查看框架附带的可执行二进制文件、文件生成器以及其他框架内流行的工具。
二进制文件和可执行文件
二进制文件和脚本文件有助于框架开发,有时也作为框架用户的接口。这些脚本可以包括构建步骤、自动化和其他与 JavaScript 相关的任务。在某些情况下,这些可以是辅助脚本,也可以作为编码过程的一部分持续运行。通常,这些是用 JavaScript/TypeScript 编写的,以确保跨平台执行并保持与同一语言的流程一致性。
当今的框架具有各种可执行任务,你可能会发现简短的 npm 命令或完整的脚本文件。这些可执行文件可用于以下目的:
-
构建和发布:这涉及到发布框架的新版本。在本章的开头,在 探索核心架构 部分中,我们看到了基于相同源生成的多个框架版本示例。这里,一个良好的构建脚本可以更新变更日志文件并创建源版本标签,这会很有用。此工作流程还可以涉及生成静态资产和上传框架工件。一个简单的例子可以在 Ember.js 中找到 (github.com/emberjs/ember.js/blob/master/bin/build-for-publishing.js)。
-
完整的测试运行器:一个可执行文件,用于运行测试或其他测试集成。框架有许多类型的测试,通常创建一个可以设置测试环境并快速执行所有或所需测试的脚本非常重要。
-
开发工作流程:一个用于快速开始框架开发的脚本。通常,这包括启动 JavaScript 打包器、文件监视器和有时是开发服务器。
-
管理依赖项:在这里,你可以安装和重新构建依赖项。鉴于框架的许多部分可能位于单独的包或存储库中,自动化依赖项管理过程变得更加高效。
-
代码风格检查和代码覆盖率:与测试运行器类似,代码风格检查和代码覆盖率确保良好的代码质量标准。这些工具分析源代码并追踪语言的不正常使用。代码覆盖率工具确保测试运行通过框架代码的所有路径。
JavaScript 框架使用 package.json 的脚本字段 (docs.npmjs.com/cli/using-npm/scripts) 来定义一组常用脚本。在现有的框架中,你可能会发现该字段中定义的脚本列表可能非常庞大——例如,Vue.js 在核心包文件中定义了超过 30 个脚本命令。Angular 创建了一个名为 ng-dev 的工具 (github.com/angular/dev-infra/tree/main/ng-dev) 来管理所有开发任务。
AdonisJS (github.com/adonisjs/core/blob/develop/package.json) 拥有一份相当短的脚本列表,同时也很好地展示了框架可能需要的脚本示例。在这里,我们可以看到发布工作流程、测试、代码风格检查等示例:
"mrm": "mrm --preset=@adonisjs/mrm-preset",
"pretest": "npm run lint",
"test": "node -r @adonisjs/require-ts/build/
register bin/test.ts",
"clean": "del-cli build",
"build": "npm run compile",
"commit": "git-cz",
"release": "np --message=\"chore(release): %s\"",
"version": "npm run build",
"prepublishOnly": "npm run build",
"lint": "eslint . --ext=.ts",
"format": "prettier --write .",
其中一些脚本是一些快捷方式,例如clean任务,并调用其他工具执行操作。
在构建你的框架的过程中,确定你在框架开发和新版本发布时执行的一些常见任务是一个好主意。一旦你做到了这一点,为这些任务创建一系列定义良好的脚本。这将导致一个更加组织和愉快的开发工作流程。
文件生成器
你经常会发现,工具被提供出来以生成项目的常见组件的代码。通常,这些工具“生成”必要的骨架文件,开发者可以稍后修改这些文件以添加自定义的业务逻辑。如果你将这种脚手架功能添加到你的框架中,那么它将允许开发者减少手动编写的代码重复,并通过防止手动编写项目部分时引入意外错误来节省时间。通常,生成器还会创建测试文件并配置测试运行器,这又是节省时间的一个方面。这些生成器命令通常与框架捆绑在一起,并通过命令行界面提供。
JavaScript 框架通过允许应用程序被脚手架或允许开发者在项目进展过程中构建额外的组件来采用这种生成器模式。例如,Angular 使用图表(angular.io/guide/schematics)来生成代码。它为其实体内置了图表,但也允许开发者编写自己的图表。通过这个 Schematics API,你可以创建自定义任务来在应用程序项目中执行,包括完全操作文件和目录的能力。
在另一个例子中,Next.js 提供了一个名为 create-next-app 的应用程序脚手架工具(github.com/vercel/next.js/tree/canary/packages/create-next-app),它允许框架用户快速开始使用 Next.js 构建应用程序:
> npx create-next-app@latest
Need to install the following packages:
create-next-app@13.1.6
Ok to proceed? (y) y
✔ What is your project named? ... framework-architecture
? Would you like to use TypeScript with this project? > No / Yes
Next.js 生成器是使用常见的 JavaScript 模块构建的,就像许多网络框架一样,它利用 Node.js 来实现这类工具。
根据你打算如何使用你的框架,你必须选择你想要提供的生成器功能类型。如果你的框架是大型内部项目的一部分,并且通常不用于创建新的应用程序项目,那么类似于 Angular 的图表方法会更合适。
在 第一章 中,我们看到了许多框架,如 Gatsby,都是静态站点生成器。这是一个框架可以依赖于基于某些文件生成工具的站点生成器的另一个用例。这种模式可以以与任何生成器相同的方式受益——抽象复杂性,消除重复性任务,并减少维护。
编译器
在一般计算中,编译器将某种类型的源代码转换为另一种目标源代码。JavaScript 在这个方面有很多不同的编译器工具,其中许多工具以不同的方式应对 Web 开发的挑战,同时适应最新的架构趋势。随着时间的推移,越来越多的框架开始使用某种类型的编译器进行开发,使这些项目能够享受到这些工具的好处。在本节中,我们将介绍一些目前在框架中使用的示例。
这些工具带来的开发改进和工作流程极大地造福了框架开发者。当您创建一个新的 JavaScript 框架时,您一定会欣赏利用这些工具。
框架编译器和构建器
框架结构通常是通过构建工具组装的。这一步骤的目标是获取框架所需的所有资源,对它们进行特定的优化,并输出一个针对特定运行时环境的、开发者友好的代码包。JavaScript 框架通常使用此类工具作为其构建系统。在本节中,我们将探讨一些可能的编译器和打包器选项:
-
tsc是一个二进制文件,可以用来构建和分析 TypeScript 文件,并将它们转换为 JavaScript 文件。 -
Webpack:这是一个可以将 JavaScript 和其他与 Web 开发相关的文件进行多路复用的打包编译器。由于其流行和丰富的功能,Webpack 支持许多高级开发特性。
-
Turbopack:这是由 Webpack 的作者用 Rust、Go 和 TypeScript 编写的 Webpack 后继者,它由一个打包器和增量构建系统组成。与 Webpack 类似,Turbopack 工具链专注于将您的开发资源打包成优化的包。Turbopack 使用 Rust 编程语言来实现更快的构建,特别是对于大型项目。
-
esbuild:这是一个用 Go 编程语言编写的工具,它通过并行化工作负载来创建 JavaScript 包。
-
Babel:这是一个用于转换和生成新 JavaScript 语法到兼容旧语法的工具链,同时专注于跨浏览器支持和各种 JavaScript 环境的支持。您可以将它包含在框架的构建管道中,使其在旧浏览器中也能运行和测试。
-
rollup:这是一个用于创建优化后的 JavaScript 包的模块打包器,它拥有庞大的配置和插件生态系统。由于其低开销和输出灵活性,它非常适合框架使用。
-
Parcel:这是一个专注于零配置或最小配置的打包工具。Parcel 集成了许多内置优化,并自动将流行的源类型转换为 JavaScript。Parcel 可以用来生成包含业务逻辑和框架代码的优化后的应用程序包。
-
快速 Web 编译器(SWC):这是基于 Rust 编程语言,其重点是加快 TypeScript 编译步骤。它被 Next.js 和 fresh 框架使用。
Jest – 我们在 第一章 中提到的测试框架 – 使用 Babel 构建测试框架本身。作为框架用户,你也可以通过调整 babel.config.js 中的 Babel 环境配置来选择特定的 JavaScript 目标:
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
}
上一段代码块中的预设允许 Jest 在 React Native 环境中运行,这与常规的 JavaScript 应用程序运行时不同。
框架编译器可以是核心技术的一部分,因为它使系统的所有主要功能成为可能。基于 Svelte 的应用程序使用 Svelte 编译器,它接受 .svelte 文件并输出 JavaScript 文件:
App.svelte
<script>
let bookChapter=3;
console.log(bookChapter);
</script>
给定一个基本的脚本,生成的 JavaScript 输出包括所需的 Svelte 依赖项和初始化的 SvelteComponent:
import { SvelteComponent, init, safe_not_equal } from
"svelte/internal";
let bookChapter = 3;
function instance($$self) {
console.log(bookChapter);
return [];
}
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, null, safe_not_equal,
s{});
}
}
上述生成的代码是编译器输出,将 console.log 的实例移动到实例函数中。编译后,script 标签被移除,代码被包装在一个 instance 函数中,该函数作为 App 组件的一部分执行代码。这成为由 Svelte 驱动的应用程序的入口点。Svelte 能够解析 CSS 样式块和类似 HTML 的语法。更多编译器在行动的示例可以在 svelte.dev/examples 找到。
如果框架使用将静态类型代码转换为的编译器,那么你可能需要定义和公开定义的类型。在下一节中,我们将探讨它是如何工作的。
类型
使用类型系统进行导出和构建是框架架构的另一个部分,并且在现代 JavaScript 工作流程中变得越来越流行。我们已经看到许多框架利用 TypeScript 进行其架构。TypeScript 提供了多种选项来组织这些接口和类型。它定义实现为 .ts 文件,声明文件为 .d.ts 文件。作为此架构的一部分,框架声明其 TypeScript 类型以供其内部和外部文件使用。外部类型作为文档的一部分提供给框架消费者。
框架将其类型声明作为其发布包的一部分提供。例如,整个 Solid.js 框架都是类型化的(solidjs.com/guides/typescript),就像许多其他项目一样,并且在安装时包含 types 目录:

图 3.5:Solid.js 中提供的类型声明
Solid 将类型定义作为 npm 包的一部分分发,为服务器、渲染器和反应式接口提供特定的定义。
在下一节中,我们将探讨源映射,这是另一种与框架一起分发的编译文件,旨在提高开发工作流程的效率。
源映射
关于.map文件扩展名的概念。这些映射通常作为某些编译或构建过程的一部分被创建,因此我们将在这部分学习它们。
图 3.6 展示了生成的源映射的一个片段,尽管其中大部分内容对人类来说难以阅读:

图 3.6:Angular 的 core.js 文件生成的源映射
在这里,我们可以看到生成的映射正在使用规范的第三版;映射的其余部分旨在由能够处理大部分此文件的 Web 浏览器工具进行解析。
作为前端框架发布版本的一部分,映射文件作为框架包的一部分提供,开发者需要决定如何处理这些文件。当项目部署到开发环境时,整个项目可以使源映射可用。例如,Vue.js 在开发环境中使用esbuild来打包其代码,在生产构建中使用rollup。作为构建过程的一部分,它可以为这两个实用工具传递一个选项来为输出文件生成源映射。关于esbuild的更多功能信息可以在esbuild.github.io/api/#sourcemap找到。内部,esbuild使用 Go 编程语言快速构建 JavaScript 项目,并在其github.com/evanw/esbuild/tree/main/internal/sourcemap仓库中实现了源映射规范。这只是其中一种实现方式,具体取决于你如何决定构建你的框架。你将能够找到许多为你的项目生成源映射的方法。
源映射同样在服务器端 JavaScript 中得到了应用。由于 JavaScript 抽象化编写方式的增加,Node.js 中的源映射功能可以帮助你追踪回原始代码,这些代码可能是用 ClojureScript、TypeScript 或其他语言编写的。
通常情况下,在你的框架中启用源映射支持并不困难。然而,你需要确保你正确地配置了它,确保网络浏览器工具能够正确地使用映射,并且仅在适用时才暴露源映射。
摘要
本章重点介绍了 JavaScript 框架的技术架构结构。我们关注了框架架构的三个重要部分:包(packages)、脚本(scripts)和编译器(compilers)。结合我们从第一章和第二章中获得的知识,我们可以开始精确地指出各种框架在架构结构上的核心差异。对架构模式的概述有助于我们理解现有框架是如何构建的,并使得我们构建新的框架变得更加容易。
探索现有的项目有助于我们从现有的开源框架中借鉴最佳想法。此外,学习内部设计可以深入了解框架如何适应使用框架的复杂代码库。下一章将探讨使框架开发和可用性更加完善的开发支持技术和模式。
第四章:确保框架可用性和质量
继续探讨前一章中框架架构的主题,我们将开始研究 JavaScript 框架的更多架构方面。虽然技术架构扮演核心角色并提供框架的精髓,但工程师还可以添加额外的系统架构元素,以便项目具有更高的可用性和质量。由于我们主要专注于 JavaScript 项目,我们将发现各种有助于我们关注质量的工具。这些工具通常是用 JavaScript 构建的,但它们也与其他系统集成,使得熟悉该语言的人更容易珍惜这些好处。
支持框架的技术可用性是一系列开发质量和可用性模式。这包括作为框架开发和使用的部分使用的附加基础设施。通常,我们将这些组件视为提高我们框架生命周期的工具。首先是确保框架可用性的技术,对于框架作者、贡献者和用户来说。其次是支持性基础设施,如文档和多种类型的测试。
我们将在本章中探讨这些重要主题,重点关注帮助我们构建框架的开发支持模式。就像技术架构一样,这些技能和工具可以在构建任何类型的 JavaScript 框架时应用。本章的主题包括探索以下内容:
-
框架文档 – 一套书面或生成的材料,提供有关框架功能和如何利用框架进行新项目的信息。为了向最佳实践学习,在本节中,我们将探讨其他 JavaScript 框架如何制作公开和内部文档。
-
框架测试的多样性 – 广泛用于使用不同类型的工具(如单元测试、端到端测试等)检查框架的正确性。进一步关注 JavaScript 项目,本节将探讨框架项目的测试能力。
-
开发工具 – 帮助开发过程的外部工具。这包括额外的配置和工具,这些工具有助于框架的内部工作流程,例如持续集成、源代码控制增强和开发调整。在接下来的章节中,我们将看到 Vue.js 和 Nest.js 等 JavaScript 项目使用哪些类型的开发者工具。
-
通用框架结构 – 理解我们如何根据从其他开源框架架构和本书中迄今为止看到的模式中吸取的教训来创建自己的框架结构。这将为我们提供一个很好的概述,了解作者如何在大型项目中组织 JavaScript 框架代码。
技术要求
您可以在本书的存储库中找到本章的资源:github.com/PacktPublishing/Building-Your-Own-JavaScript-Framework。为了更容易地与本章的实际部分互动,请使用 chapter4 目录。交互式脚本将使您更容易运行本章的示例。它的工作方式如下,从您的命令提示符或计算机上的终端窗口执行:
> npm install
...
> npm start
Welcome to Chapter 4!
? Select the demo to run: (Use arrow keys)
> Next.js with Tailwind CSS
Practical Docus.dev example
与其他章节一样,存储库中的代码旨在在可以运行 Node.js 运行时的操作系统上工作,例如 Windows、macOS 和大多数 Linux 变体。
JavaScript 测试复习
在本章中,我们将讨论与 JavaScript 测试相关的话题。如果您需要复习或需要更多资源来深入了解各种测试技术,请查看 Packt 出版物的额外出版物,详情请访问 subscription.packtpub.com/search?query=javascript+testing。
开发支持
让我们深入研究一系列技术工具,这些工具使开发者能够构建专注于可用性和易用性的高质量框架。学习和利用这些类型的开发支持策略将有助于我们的框架开发,使我们的项目在内部(工作项目)或公共环境中(开源/公开发布的项目)更具可用性。
其中一些开发方法和技能并不特定于 JavaScript 框架开发;它们被广泛应用于许多 JavaScript 和 Web 应用程序开发任务中。然而,在框架开发的背景下,对这些工具和可用性模式的方法与常规应用程序项目不同。例如,一个框架可能有一个特定的扩展测试集,确保新功能和更改不会破坏使用它的现有应用程序。这种扩展测试是仅适用于框架项目而不适用于应用程序项目的特定情况。
此外,一个框架可能更注重技术设计基准测试和兼容性测试,支持各种用例。这很大程度上源于框架消费者和利益相关者的需求。
在 第三章 的 核心 API 部分,我们看到了接口和功能的示例,例如 依赖注入。这些接口旨在通过其灵活性和功能集赋予框架用户权力。然而,这些接口需要文档化,以便开发者可以访问。否则,即使框架接口足够简单或强大,但不可发现或解释,可能不会被用户使用。这些接口还需要进行彻底的测试,无论是独立测试还是作为更大系统的一部分。最后,我们需要各种类型的基础设施来支持这一测试、维护和文档化过程。幸运的是,有许多现有解决方案通过 JavaScript 社区提供的工具和外部软件服务,使框架开发和维护变得更加容易。
让我们关注三个可帮助构建优秀框架的可用性和质量模式类别:
-
文档 – 针对不同框架利益相关者的材料集合。这些可能是由框架的开发者生成或编写的。此参考也可以是内部的,展示了设计决策和技术架构。
-
框架测试 – 测试基础设施对于开发、功能集和维护至关重要,因为它们确保框架质量。这包括使用各种工具,例如单元测试、端到端测试和集成测试。
-
开发工具 – 提高开发者内部工作流程的辅助工具。这些工具集成了简化项目工作的技术。它们通过引入源代码控制改进、持续集成等流程来实现这一点。
这些模式类别的每个类别都有几个子类型。在大多数情况下,多个现有框架在其项目中依赖于这些类型的技巧。我们将在本章中深入探讨这些模式。
图 4**.1 展示了 JavaScript 框架今天使用的各种文档类型、测试类型和附加工具的详细概述:
| 文档 | 框架测试 | 开发工具 |
|---|---|---|
| 公共 API | 单元测试 | 持续集成 |
| 示例 | 集成测试 | 源代码控制 |
| 内部文档 | 端到端测试 | 包配置 |
| 基准测试 |
图 4.1:框架中使用的工具和模式的子类型
我们首先关注文档,这是我们框架无法生存的东西。
文档
编写文档是框架适应性和可用性最重要的任务之一。产生的参考资料可以使开发者正确利用提供的工具。框架开发者花费了大量时间编写和调整他们的文档,专注于提供框架功能的组件最详细和最简单的解释。对于一个典型的 Web 应用,你已经有了一些关于如何运行它和配置其部分的功能文档。然而,在开发框架时,需要更多的文档,包括 API 方法、学习材料和其它解决方案,以便更容易地充分利用框架。今天,大多数框架都投资于展示框架的 API 方法,编写帮助开发者从头开始学习框架的文章,创建交互式教程,并提供详细示例,展示框架如何应对特定功能集的挑战。
一个优秀的文档范例来自 React 的创造者——在最近推出的新学习平台react.dev上,该平台鼓励在整个生态系统和框架内使用该库。为了鼓励采用和成功知识转移,他们的重点是创建与 API 参考一起的学习环境。
当你开始构建你的框架时,请记住,仅提供一个程序性 API 列表是不够的。在图 4.2中,我们可以看到有价值参考材料的优秀示例:

图 4.2:学习 React 文档
尽管 React 是一个库,但我们仍然可以从这个项目的文档结构中学到很多。它包括几个关键项。首先是安装指南;这可以包括包安装指南和使用框架搭建新项目的能力。然后,现有或潜在的框架用户将得到一个教程和工具背后的思维模型解释。最后,一系列文章解释了开发者在使用工具时需要了解的最重要的话题。
你的框架应该追求类似的学习文档形式。即使在内部框架开发的情况下,你或你的团队仍然应该记录并创建学习参考材料,以鼓励正确使用你的框架流程。这种做法引导我们进入文档的下一个重要部分——API。
公共 API
在第三章中,作为探索框架包的一部分,我们检查了框架的入口点。文档作为另一种类型的入口点;开发者通过利用提供的文档与框架交互。主要来说,这种交互可以通过公共 API或API 参考来实现,这些是从框架的代码及其接口中创建的。
在本书中,我们看到的每个框架都附带了一个与框架一起发布的 API 参考。这种类型的 API 参考可以是静态生成或动态生成的。在图 4.3中,我们看到这样一个文档的例子;Vue.js 文档是从gihub.com/vuejs/docs仓库生成的,并使用静态站点生成器组装的:

图 4.3:Vue.js API 参考的一部分
有许多开源项目可以帮助您更容易地生成和维护文档文件:
-
Docusaurus (docusaurus.io) – 一个静态文档站点生成器。特别为框架设计,它提供了搜索、根据框架发布进行版本控制等功能。
-
MarkDoc (markdoc.dev) – 另一个用于自定义文档站点的开源项目。它是可扩展的,旨在提供最佳的文档编写和发布体验。
-
Docus (docus.dev) – 一个利用我们熟悉的框架(如 Nuxt.js 和 Vue.js)的文档生成器。支持 Markdown 语法,并在许多服务上实现零配置部署。
-
TypeDoc (typedoc.org) – 用于 TypeScript 源代码的文档生成器。基于 TypeScript 文件内的注释创建静态站点。该工具还具有将解析的源代码输出为 JSON 文件的能力。类似工具JSDoc (jsdoc.app)也适用于纯 JavaScript 项目。
-
TSDoc (tsdoc.org) – 与 TypeDoc 类似的项目,由微软支持。它通过为其他工具提供基于注释的文档生成引擎,专注于文档生成器的标准化工作。与多个其他项目(如 ESLint 和 Visual Studio Code)有集成。
如果您的框架刚刚起步,使用我们刚刚列出的工具可能有些过度,但鉴于文档对于框架的可用性至关重要,您需要确保使维护可读性和干净的文档变得更容易。您还可以从像 Angular 这样的更大、更复杂的框架中汲取灵感。该项目已经提供了深入的 API 参考(angular.io/api),但除此之外,作者还提供了一份关于概念、错误类型和诊断的详尽参考。所有这些都可以在 Angular 网站的指南部分(例如,angular.io/guide/file-structure)中找到。
随着您开发框架,您可能会在版本与版本之间引入重大的破坏性更改。如果您处于已经存在现有框架消费者的环境中,那么您应该花时间创建一个迁移参考文档。良好的迁移指南有助于您的利益相关者跟上变化并使用最新的修复。一些好的迁移指南示例包括 Electron 的破坏性更改指南(electronjs.org/docs/latest/breaking-changes)和 Express 迁移概述(expressjs.com/en/guide/migrating-5.html)。
文档工具的实际应用
本章的仓库目录中包含了一个文档工具实际应用的示例。在这个例子中,示例使用了 Docus,Nuxt.js 框架为项目的后台基础设施提供动力。您可以在自己的电脑上通过在chapter4目录中运行交互式脚本或手动导航到practical-docus并运行npm install,然后npm run dev来尝试这个示例。文档站点将在端口3001上运行,您可以通过编辑目录中的文件来实时编辑和与文档工具交互。
基本文档可以极大地改善您的框架体验,但您还可以做更多的事情来使适应变得更加容易。在下一节中,我们将重点讨论提供框架使用示例的重要性。这些有意义的资源可以突出框架的强大元素以及与其他系统的集成简便性。
示例
提供示例极大地有助于减少适应框架最具挑战性的方面之一——学习曲线。为了鼓励采用并减少摩擦,框架开发者投入时间来制作框架使用的示例。这些可以作为参考文档的一部分,或者与框架源代码一起提供。如果您正在开发一个内部框架,投资于示例仍然是有益的。如果您的项目将被许多内部团队或新员工使用,维护一个示例的基础可以减少问题数量和困惑。
在我参与 JavaScript 测试框架贡献的经验中,最有效的开发投资之一就是专注于创建集成指南和开发示例。这对于测试框架项目尤其重要,因为这些资源的可用性使得开发者更容易将测试框架集成到他们的系统中。它还展示了项目的成熟度,表明它能够与许多不同的系统协同工作。这不仅仅是我的经验,几乎所有 JavaScript 框架都专注于提供即时可运行的示例。这些项目利用 StackBlitz (stackblitz.com) 和 CodePen (codepen.io) 等工具,使潜在的开发者能够在几秒钟内进入框架环境。例如,访问 stackblitz.com/fork/angular 会为你提供一个现成的 Angular 框架应用程序。
为了获得更多灵感,Next.js 考虑到示例的思考方式;该框架维护着超过 50 个示例,位于 github.com/vercel/next.js/tree/canary/examples。这些示例包括对 GraphQL 支持、CMS 和博客用例、与其他工具的集成以及部署目标。为了快速启用用户运行示例,create-next-app CLI 支持使用 example 参数基于示例来构建:
npx create-next-app --example with-tailwindcss-emotion next-example-app
在为您的框架开发示例时,请记住,您将需要维护您创建的所有示例,就像文档参考一样。如果某些示例代码过时且不再按预期工作,那么这将给作为框架维护者的您带来更多负担。
Next.js 的实际示例
您可以在 next-example-app 目录下的 chapter4 目录中查看此示例。按照 README.md 文件中的指南设置 Firebase 项目。该项目需要 Firebase 项目凭据才能正常运行。要初始化 Next.js 应用程序,请运行 npm install 然后运行 npm run dev。您还可以直接从 chapter4 目录使用交互式脚本。
要打开应用程序,请使用 localhost URL,这可能是 http://localhost:3000。按照终端输出中的说明操作。要编辑文件,请使用 Visual Studio Code 打开 next-example-app 项目目录。
根据您框架的性质,您可以使用 JavaScript 运行时工具为您自己的框架创建一个游乐场环境。在 *图 4**.4 中,我们看到 Vue.js 组件游乐场的示例;这种环境将示例的概念进一步发展:

图 4.4:Vue.js 单文件组件游乐场
每一个“游乐场”示例,你都可以使用最基本的功能来教授框架,从更高级的使用案例。
当你开始自己的框架时,最好将你的示例作为框架存储库的一部分。为了减轻维护负担,确保你在测试基础设施中执行你的示例(更多内容将在第十章,有关框架维护)中。如果你独自或与小型团队一起工作,在你的框架中使用包含的示例可以极大地提高开发过程,帮助你快速迭代。
内部文档
内部文档的目的是帮助框架作者继续开发框架。即使你是框架的唯一作者,维护内部文档仍然很有用,即使是为了你自己的利益。这样,你可以回顾过去的代码和设计决策,并使更新你的项目更容易。主要来说,这种类型的文档不应该被框架用户或利益相关者消费。然而,对于调试用例,公开这些材料仍然是有用的。
内部文档可能包括内部模块的详细接口。它可能描述内部实现的原理。例如,Nuxt.js 框架在其参考页面上结合了面向公众和内部文档。框架的渲染器、构建器、生成器和其他类在内部术语表中进行了描述(github.com/nuxt/nuxtjs.org/tree/main/content/en/docs/6.internals-glossary)。例如,Nuxt 提供了自己的模块系统(nuxtjs.org/docs/directory-structure/modules)以扩展框架功能,该功能的内部实现由 ModuleContainer 类支持。这个类是框架内部的一部分,应该仍然被记录。它还使外部开发者能够理解和扩展框架,进行插件开发。
在 Vue.js 中,可以看到使用这种类型文档的另一个框架示例。该框架内部使用 TSDoc 工具来确保其函数的规范,例如共享实用方法。
以下代码是从框架的开发者工具存储库中提取的(github.com/vuejs/devtools/blob/main/packages/shared-utils/src/util.ts),它是框架开发者在浏览文件时可以使用的文档注释的简单示例,可以稍后导出到外部文档,或者在使用此辅助函数时由 IDE 预览:
/**
* Compares two values
* @param {*} value Mixed type value that will be cast to string
* @param {string} searchTerm Search string
* @returns {boolean} Search match
*/
function compare (value, searchTerm) {
return ('' + value).toLowerCase().indexOf(searchTerm) !== -1
}
贡献指南也是这类内部文档的一部分。对于开源和封闭框架,都可能有人想要对框架进行更改或贡献,无论是帮助你修复问题还是引入新功能。贡献文档有助于实现这一点,提供了快速编写和测试新框架更改的步骤。作为贡献说明的一部分,通常很重要列出以下几项重要信息:
-
首先,如何修改框架、构建它以及测试它。这包括指向所有使开发过程更易于接近的相关脚本。
-
其次,如何在开源和内部环境中成功编写补丁,包括遵循源代码控制指南和提交历史规则。
-
为了使框架贡献更容易,这类文档应提及关于公共和内部 API 的编码规则、文件格式化以及其他潜在的风格指南。
例如,Ember.js 在其贡献指南页面guides.emberjs.com/release/contributing上有一个页面,其他框架如 Angular 在其 GitHub 存储库github.com/angular/angular/blob/main/CONTRIBUTING.md中包含一个CONTRIBUTING.md文件。
框架测试
就像任何软件项目一样,框架需要一系列测试来确保框架按预期工作。在框架的上下文中,你会找到许多针对正确性、性能以及需要处理所有可能使用场景的特殊框架用例的深入测试。我们在第一章的测试框架部分看到了测试框架的例子;这些可以在我们的框架内部使用,以简化测试工作流程。在本节中,我们将探讨 JavaScript 框架内部使用哪些技术来确保最终框架产品具有高质量。
单元测试
就像大多数软件项目一样,框架也包括针对其接口的单元测试。它们使用与我们在第一章中看到的类似的测试框架。通常,你会看到这些类型的测试被称为“specs”,这意味着它们是规范测试。这意味着,对于框架的某个组件,存在一个它应该遵守的技术规范。在框架的上下文中,全面的测试有助于更快地重构主要组件。开源框架在接收外部代码贡献时,也受益于良好的单元测试套件。当存在大量现有测试和新测试作为变更的一部分被添加时,审查代码变更并对其有信心要容易得多。
根据 JavaScript 框架的类型,测试环境和测试挑战可能会有所不同。在针对浏览器的框架中,单元测试需要模拟浏览器和 Web API。例如,Angular 引入了几个内部测试接口,以简化与注入到 DOM 中的组件一起工作。Angular 的“变更检测”和其他与 DOM 相关的功能使用这些测试接口来抽象处理document对象实例。例如,Angular 开发者创建了几个测试包装器,以便更容易地将框架的节点树附加到 DOM 主体,如下面的函数所示:
export function withBody<T extends Function>(html: string, blockFn: T): T {
return wrapTestFn(() => document.body, html, blockFn);
}
change_detection_spec.ts 文件依赖于测试工具中的 withBody 辅助函数;这些工具依赖于在一个存在 document 对象的环境中执行许多框架的测试。
在后端框架中,项目可以选择模拟现有接口或创建仅用于测试的类。例如,Nest.js 定义了一个 NoopHttpAdapter 类 (github.com/nestjs/nest/blob/master/packages/core/test/utils/noop-adapter.spec.ts),它扩展了之前在第二章的“后端抽象”部分中看到的 AbstractHttpAdapter。以下代码显示了测试适配器的结构,以便更容易在框架测试中使用它:
export class NoopHttpAdapter extends AbstractHttpAdapter {
constructor(instance: any) {
super(instance);
}
close(): any {}
initHttpServer(options: any): any {}
// …
}
}
这个 HttpAdapter TypeScript 类在框架的规范测试中使用,以确保主要的 Application、Routing 和 Middleware 类按预期工作。
在开发 JavaScript 框架时,确保对每个新添加的组件或接口进行单元测试。这个过程将以多种方式帮助你:
-
这将提高代码质量,并帮助你组织框架,使其组件更好地协同工作。
-
框架开发过程充满了持续的重构或更改。随着框架的增长,你的单元测试套件也会增长,并且随着你编码,你将增加对更改的信心。
最后,确保你的单元测试套件运行高效。例如,Vue.js 使用 Vitest 测试运行器。Vue 有超过 2,500 个单元测试,大约在 20 秒内执行。你的框架单元测试应该尽可能快地运行,以便在你忙于开发新框架功能时提供快速的反馈循环。
集成测试
集成测试的目的是为了测试框架的多个接口或组件如何协同工作。这些测试可以捕捉到单元测试/规范测试无法检测到的问题,因为这些类型的测试旨在单独测试组件。集成测试模拟组件之间的交互,确保功能能够良好地协同工作。
在框架的背景下,内部核心架构必须相互匹配。这意味着集成测试将验证这种行为。例如,对于一个全栈框架来说,一个好的集成测试将确保当调用特定的router路由时,组件会被渲染。这种测试确保所有这些组件都能良好地协同工作。
此外,框架通常需要与其他系统集成。这意味着开发者还需要在框架和外部系统之间产生集成测试。例如,Gatsby 框架为其静态站点渲染、命令行界面和缓存基础设施提供了集成测试(github.com/gatsbyjs/gatsby/tree/master/integration-tests)。这些测试验证了框架的功能。然而,Gatsby 还包括集成测试来验证它与其他技术的兼容性。该框架有一个集成测试来验证与 JavaScript ESM 模块标准的兼容性。
编写集成测试可能具有挑战性,因为你必须验证所有类型的接口组合是否能够无缝协同工作。尽管它是框架开发过程中的一个基本部分,但如果你在匆忙交付新的框架项目,这种类型的测试最终可能比单元测试更有益。
端到端测试
端到端测试评估框架作为一个整体系统的工作情况。通常,这些测试模拟了几乎真实的用户交互。对于一个前端框架来说,创建这些测试通常意味着配置一个端到端测试框架。对于服务器端框架,端到端测试通常模拟对由框架驱动的服务器的真实请求。类似于其集成测试集,Gatsby 也维护了一套transition、grid和tree-view接口。这些可以在github.com/vuejs/core/tree/main/packages/vue/tests/e2e找到。这些测试使用 Puppeteer 在无头 Chrome 浏览器中执行命令,从而模拟真实的浏览器和用户行为。
一个广泛的端到端测试套件可以通过几种方式支持你的框架开发:
-
捕获整个系统的回归,例如模拟常见的框架命令和预期功能。
-
确认在您进行更改和开发新功能时,框架的所有组件都能协同工作。
-
将性能测试集成到端到端测试中,以便能够检测框架的缓慢性能。
-
确保框架能够正确地与外部系统协同工作。这些系统可以包括不同类型的网络浏览器或不同的后端环境。
另一种对许多当前项目都相关的框架测试形式是基准测试。
基准测试
基准测试的过程是在你框架的特定场景上运行一系列评估和试验。这些基准测试可以由框架作者或第三方编写。为了构建框架的目的,我们专注于前者,即框架将其一系列基准测试作为其内部测试的一部分。框架可以在诸如渲染特定组件配置等任务上通过基准测试分数进行竞争。对于后端框架,内存利用率、请求吞吐量和延迟等因素通常会被基准测试。为了获得灵感,Nest.js 在 github.com/nestjs/nest/tree/master/benchmarks 维护了一套基准测试工具,以比较框架提供的 HTTP 服务器的性能。在其他类型的框架中,如应用开发(Electron)和测试框架,基准测试也集中在性能上。正如我们在本章前面的 框架测试 部分所看到的,测试框架本身需要尽可能高效地执行测试套件。
对于 JavaScript 框架来说,在运行时对代码的性能进行基准测试是至关重要的。在浏览器运行时,基准测试主要关注高效的渲染和大量输入的处理。在完整的全栈 Next.js 框架中,作者包含了一些基准测试脚本以测试各种功能(位于 github.com/vercel/next.js/blob/canary/bench)。当你开发自己的基准测试时,请记住你可能不需要任何复杂的工具。相反,你可以依赖运行时的内置方法——在这种情况下,是 Node.js。
图 4**.5 展示了一个简单的基准测试脚本 github.com/vercel/next.js/blob/canary/bench/recursive-copy/run.js):

图 4.5:Next.js 仓库中的基准测试
在 图 4**.5 中,主函数提供了一个递归复制实现给测试。使用两种不同的实现执行 run 函数,为我们提供了这两个函数的比较结果。
对于后端框架,内存利用率、请求吞吐量和延迟等因素通常会被基准测试。为了获得灵感,Nest.js 在 github.com/nestjs/nest/tree/master/benchmarks 维护了一套基准测试工具,以比较框架提供的 HTTP 服务器的性能。在其他类型的框架中,如应用开发(Electron)和测试框架,基准测试也集中在性能上。正如我们在本章前面的 框架测试 部分所看到的,测试框架本身需要尽可能高效地执行测试套件。
作为框架开发者,你应该专注于为两种用例设置基准测试:
-
首先,基准测试针对你框架公开的公共接口。这些将允许你衡量你的框架完成任务所需的时间。
-
其次,你希望深入了解框架内部的微基准测试。这些内部基准测试有助于优化框架核心的特定部分,从而提高内部函数的速度。
随着你进一步开发你的项目,关注你的基准测试的测量结果,确保你不会降低框架的速度。
开发工具
框架开发和发布流程可以从包含提高框架项目质量和可用性的额外工具中受益。这些工作流程可以应用于框架的各个方面,例如依赖管理、测试、编辑器配置、格式化等。我们已经在 第三章 的 二进制和脚本 部分看到了类似的方法,依赖于额外的脚本和工具。可以改善我们框架开发周期的额外工具包括引入持续集成步骤、改进源控制和添加包级实用工具。
持续集成
作为开发周期的一部分,就像许多 Web 应用程序项目一样,框架配置 持续集成 (CI) 步骤来测试代码更改和新版本。这些 CI 系统运行所有类型的测试,如本章 框架测试 部分中提到的测试。使用版本控制系统提交的每个更改都必须通过现有的测试套件。这确保了更改不会引入破坏性更改或错误。除了运行测试之外,CI 还运行其他类型的分析,如格式检查、linting 等。这些确保了一致性、可用性和质量。
如果我们专注于框架开发,CI 有一些特殊用途。它确保框架在不同的 JavaScript 环境中正常工作。对于一个前端框架来说,这意味着在不同的平台上执行各种浏览器的测试。浏览器支持测试是双向的——新功能必须在旧浏览器版本中工作,而新浏览器不应该破坏现有框架的任何功能。运行在后端的 Node.js 和 Deno 框架跟踪新的运行时版本,遵循 github.com/nodejs/release 发布计划。在 CI 中运行这些兼容性检查是最佳解决方案;CI 平台允许快速启动不同版本的环境,并在此环境中并行化测试执行。
除了在各种环境中关注测试之外,CI 步骤还可以运行一系列依赖于你的框架的项目测试。例如,它可以生成并运行一个示例应用程序或外部脚本,应用新的框架更改。这样,它可以检查更改是否兼容。
根据框架 CI 配置的不同,集成故事可能会有所不同。在 图 4.6 中,我们看到四个成功的检查;这是 Vue.js CI 管道的一部分:

图 4.6:Vue.js 报告其 CI 状态
在 Angular 的情况下,在代码更改可以合并到存储库之前,CI 管道中存在超过 20 个检查。越来越多的集成步骤的原因是执行任务而不是测试。这些可能包括格式化、拼写和 JavaScript 代码可用性检查。
CI 步骤的复杂性和类型可能有所不同,它们还可以为您的框架发布过程做出贡献。无论您正在开发哪种类型的框架,内部或公共的,都强烈建议将 CI 步骤配置为框架开发的一部分。这种方法将确保代码质量并帮助您在框架开发中保持效率。
源代码控制
对于框架,使用源代码控制类似于使用它进行其他应用程序项目。尽管如此,JavaScript 框架依赖于源代码控制工具来标记框架发布和跟踪功能开发分支。在此背景下使用源代码控制要深入一些。例如,框架作者可能需要为框架的旧版本编写补丁,这意味着需要回退到旧的 Git 标签以引入该更改。在许多情况下,大型框架重构也发生在临时的 Git 分支中。
大多数 JavaScript 框架也会配置补充的源代码控制脚本,以改善新功能和更改开发时的工作流程。在图 4**.7中,我们看到 Nest 框架使用 Git pre-commit 钩子来执行 JavaScript 代码检查脚本:

图 4.7:pre-commit 钩子配置
在此情况下(图 4**.7),pre-commit 钩子强制在提交更改之前执行代码质量标准。使用名为Husky(typicode.github.io/husky)的 JavaScript 模块配置此类行为的步骤被简化了。您将在许多框架中找到这种模式,因为这是使开发过程更加友好的便捷补充。
到这一点,您将使用源代码控制来管理您的新框架是肯定的。然而,您可以通过学习本书中看到的一些现有框架来投资额外的工具,以改善您的编码工作流程。
包配置
package.json文件和框架目录根目录中的附加文件定义了项目的包配置。此类配置的数量可能因您在框架中使用的工具类型而异。
Nest.js 的包配置由许多工具组成,例如 ESLint、Git、npm 和 Gulp 等。类似于图 4**.8中看到的 Nest.js 配置,package.json文件将是您框架的开发入口点:

图 4.8:Nest.js 的包配置
package.json 文件包含有关框架、其依赖项以及由其他工具使用的辅助配置的信息。例如,Nest.js 的包文件 (github.com/nestjs/nest/blob/master/package.json) 存储了 nyc 代码覆盖率工具的配置、mocha 测试运行器的配置以及变更日志工具的命令。除了这些配置条目之外,包文件还有一个 scripts 对象。此对象包含在框架开发期间可以使用的命令。在 Nest.js 的情况下,这些命令执行以下一些操作:
-
构建 – 编译或构建框架的命令。Nest.js 将 TypeScript 编译器作为此命令的一部分执行。
-
清理 – 一个快速命令,用于清理框架项目的工作目录。通常,这意味着删除任何生成的或构建的文件。
-
测试 – 运行框架中包含的所有类型测试的命令。在 Nest.js 和许多其他框架的情况下,这些类型的命令通常根据它们运行的测试类型进行拆分。
-
代码风格检查 – 分析项目中的 JavaScript 代码,寻找编码风格错误、陷阱和潜在问题。Nest.js 使用 ESLint,并行运行以快速诊断框架文件。
-
npm. -
安装 – 安装项目的依赖项。在 Nest.js 的情况下,依赖模块提供了运行后端服务所需的功能。开发者依赖列表包含所有用于框架项目的基础设施模块。
-
覆盖率 – 运行测试代码覆盖率工具以确定是否需要更多测试来完全覆盖所有框架逻辑。例如,Nest.js 使用 Istanbul (istanbul.js.org) 生成代码覆盖率报告。
这不是一个详尽的列表,但它为你的项目中可以包含的命令类型提供了一些灵感。package.json 文件的 scripts 部分通常遵循 docs.npmjs.com/cli/using-npm/scripts 上的参考材料,但不同的 JavaScript 包管理器可能对这些命令的处理略有不同。你的框架应该利用 package.json 的优势,创建快速可访问的脚本,并将 package.json 文件配置为框架开发工作流程的入口点。
正如我们在本节中看到的,有许多开发工具可以增强框架开发,并且对于使项目成功至关重要。这些开发模式已经经过多年的改进,并且现在已深深嵌入到许多 JavaScript 项目中。在下一节中,我们将探讨框架结构的整体图景,这将为我们自己的框架项目提供一个坚实的轮廓。
从其他架构中学习
在当前和前几章中,我们看到了框架使用的技术结构、工具和模式。如果我们浏览在第一章的“框架展示”部分收集的框架源代码,我们可以开始清楚地看到重复的模式。遵循这些实践,我们可以在自己的框架开发中利用它们。通过从不同类型的 JavaScript 框架的现有设计中汲取知识,我们可以构想出一个结构系统,它在我们构建项目时能为我们提供良好的服务。我们可以将这些方法和实践融合到一个通用的框架结构中。
以下代码显示了通用的 JavaScript 框架结构:
<root framework directory>
| <main framework packages>
+ <core framework interfaces...>
+ <compiler / bundler>
| <tests>
+ <unit tests>
+ <integration and end-to-end tests>
+ <benchmarks>
| <static / dynamic typings>
| <documentation>
| <examples / samples>
| <framework scripts>
| LICENSE
| README documentation
| package.json (package configuration)
| <.continuous integration>
| <.source control add-ons>
| <.editor and formatting configurations>
本项目结构应有助于阐明我们的框架项目结构的处理方法,并赋予您作为开发者设计自己项目结构的能力。框架文件和目录结构是前两章的总结,结合了我们迄今为止看到的大部分组件——框架包、编译器基础设施、框架测试、文档、脚本等。未来的章节将使用这个结构来构建我们的框架。
当我们查看核心架构和框架项目的示例时,有助于我们形成我们的框架将包含的内容和外观。在当前架构示例中我们看到的一切并不一定都是我们的框架功能或成功所必需的。实际上,如果您只为内部项目构建框架,那么您将选择不同的工具组合来帮助您进行开发。
摘要
本章介绍了框架文档的重要性、提高稳定性的各种测试以及建立高效框架工作流程的内部工具。对良好文档的投资有助于框架作者和框架用户。缺乏明确定义的文档可能会对框架的成功造成毁灭性的影响。幸运的是,有许多工具可以帮助简化文档的方法。同样,存在用于测试工作流程的工具,涵盖了框架内代码测试的所有方面。最后,额外的工具,如改进的源代码控制和编辑器配置,使得在框架上工作变得更加愉快,并帮助作者专注于框架内部。所有这些开发支持因素在框架开发和架构中发挥着至关重要的作用。从其他项目学习并利用支持开发过程的模式可以帮助我们扩展我们的架构技能并提高效率。
到目前为止,我们已经了解了大量现有的框架技术,这些技术能够支持针对 JavaScript 运行时的 Web 应用程序和后端服务开发。在接下来的章节中,利用这些知识和对现有框架项目的详细了解,我们将深入探讨我们框架构建的各个方面。这意味着从头开始启动一个全新的项目。利用从现有项目中学到的模式、抽象和经验教训,我们能够体验到构建我们自己的框架需要哪些要素。
下一章将重点介绍框架作者在开始新项目之前需要考虑的一些因素。
第二部分:框架开发
在本部分,本书基于现实世界的框架示例,并转换方向,专注于从头开始创建框架的编程方面。目标是涵盖规划、架构和发布全新全栈框架的完整过程。这些阶段包括几个重要的考虑因素和关于各种类型组件架构的教训。重点是每个过程步骤的实用方法和指导,这对所有类型的开发者都有益。
在本部分,我们涵盖了以下章节:
-
第五章,框架考虑因素
-
第六章,通过示例构建框架
-
第七章,创建全栈框架
-
第八章,架构前端框架
第五章:框架考虑事项
在前面的章节中,我们主要专注于从其他框架项目中学习,为构建我们的全栈 JavaScript 框架做准备,该框架将包括创建后端基础设施和前端界面的能力,并将具备测试这些功能两方面的能力。尽管我们的目标是全栈框架用于应用程序开发,但您将能够将从这个经验中学到的知识应用到类似的 JavaScript 项目中。现有项目的架构模式和设计决策将帮助我们定位我们的项目,并为其成功奠定基础。在本章中,我们将研究在规划我们的框架时需要考虑的三个因素,这些因素对有抱负的软件架构师和考虑成为大型技术项目决策背后负责人的个人来说都是有用的。
为了我们框架考虑的目的,以下列出需要涵盖的项目:
-
确定项目目标:关注你正在构建的内容以及框架 API 的主要消费者和赞助者。
-
识别框架问题空间:与您正在开发的框架的新问题空间相一致。
-
技术设计决策:塑造您框架独特性的因素,如技术栈、架构和开发方法。
在本章以及本书中,我们正在考虑一种框架构建的教育方法,这意味着未来的章节将涵盖特定类型的 JavaScript 框架的开发,重点关注 Web 应用程序系统。然而,你可以利用所获得的知识来构建满足你特定需求的框架。
以下图像将帮助我们关注考虑类别,并突出显示在规划和开发周期中作为有用信息的特定子部分:

图 5.1:框架开发支柱
技术要求
技术要求与前面的章节类似。使用本书仓库中的chapter5目录运行npm install,然后运行npm start,以快速开始本章中提到的代码和示例。
确定项目目标
在考虑构建新框架时,您必须确定您项目的目标和利益相关者。这两个因素是您在构建新事物的时间和投资背后的主要驱动力。开始一个新的框架项目需要了解潜在的动机,并对目标有清晰的洞察,强调您支持的开发者及其需求。这些原因可能从内部工作用例到开源爱好项目不等。您的场景可能非常不同,但根据图 5.1,我们可以探索本节作为第一支柱的项目目标。
上下文
理解项目的背景对于指导其发展和确保其满足目标用户的需求至关重要。背景涉及评估诸如项目目的、目标受众以及它将在其中使用的环境等因素。框架项目的两个背景主题如下:
-
与工作相关的公司支持的项目
-
公共开源项目,通常具有教育或爱好性质
每种场景都伴随着其独特的需求和考虑因素。
为特定商业需求开发的框架可能具有从小型初创环境到大型企业的各种商业目标。在构建满足商业需求的框架的背景下,专业用例可能会有很大差异。例如,与 React 库的初始开发类似,一个项目可以支持单个平台,如 Facebook。然而,商业需求也可能包括为重复用例开发框架,例如部署具有相同核心架构的多个服务。
爱好/开源项目可以从概念验证(PoCs)或学习材料发展到软件行业广泛使用的工具。这些通常不会产生经济影响,但它们可以作为您职业和实际知识的增长工具。专注于在特定的软件开发和 JavaScript 知识领域扩展您的技能集。
在编写任何代码之前确定并调整您新框架的项目目标是明智的。如果您的项目专注于内部公司用例,例如支持内部公司产品,那么目标将更多地针对提高效率、改善协作和简化开发流程。最终,所有规划和建设都是为了支持主要核心目标——使组织能够更快地交付更高质量和更可靠的项目。
一旦您的框架进展,背景可能会发生变化。例如,如果框架最初是一个内部项目,那么在未来的某个阶段,您可以将其开源,并利用与之无直接关联的其他开发者的反馈。反之亦然——一个作为爱好项目开始的框架可能会被用于内部专业用途,并得到商业客户的投资支持。这两种情况在软件开发行业中都很常见。
利益相关者
您项目的目标受众和利益相关者是您的框架客户。他们是那些期望使用简单且具有充足编程接口的系统的人。满足他们的需求和期望对于您框架的成功至关重要。这些用户正在寻找一个易于使用且提供高效编程接口的系统,使他们能够轻松敏捷地开发应用程序和服务。
为了创建一个吸引目标受众的框架,你对利益相关者支持的投入对于确保开发体验直观且用户友好至关重要。利益相关者支持包括设计清晰、组织良好的资源,并提供解决日常问题的示例代码和用例。通常,你需要直接或通过其他方式为你的利益相关者提供支持。通过使开发者更容易理解和导航你的框架,你将鼓励适应性。
满足你受众多样化需求的强大编程接口也起着重要作用。通过提供实用、可适应和高效的工具,你将赋予用户自信地应对他们的项目,并培养对框架的信任和忠诚感。
作为框架开发者,请记住,你的受众的需求和期望应该始终处于你的设计过程的最前沿,因此你的目标应该是提供一种以用户为中心的体验,这种体验在竞争激烈的市场中脱颖而出,并成为开发者和利益相关者的无价资产。
在本书的背景下,框架以读者作为利益相关者,专注于教育材料。如果你正在跟随并创建自己的框架,请将自己视为利益相关者。这使得事情变得简单得多;你拥有改变和改变框架路径的自由。
在下一节中,我们将探讨一些其他在开始编码之前可能有益于反思的考虑因素。
其他考虑因素
在框架开发中需要考虑的额外项目与小型或大型团队开发软件项目非常相似。在确定你的项目是否需要存在以及是否应该构建时,以下主题中的有用问题可以作为考虑的一部分:
-
框架的一般目的:这源于核心目标;你应该清楚地了解交付你的项目的首要原因,确定特定的软件相关问题并旨在解决它们。
-
现有工具的调查:在从头开始构建新事物之前,评估生态系统中的现有项目是一个好主意。这种考虑将帮助你决定是否内部使用一些工具或扩展它们以满足你的需求。
-
维护成本:根据解决你框架问题空间所需资源的规模,你需要了解项目的时间和金钱投入。预测这一点将帮助你合理分配资源并确保项目的长期可持续性。
-
创新和独特功能:确定你的框架相对于生态系统中的现有解决方案提供的独特卖点和发展优势。这可能包括高级功能、增强的性能或解决典型问题的未探索方法。
-
资源:评估你的开发团队的技能、专业知识和可用性,并确定可能需要解决的任何差距。这可能涉及雇佣新团队成员或寻求外部支持,以确保框架的成功开发。在某些情况下,你可能成为推动整个项目发展的唯一资源。这带来了高效设计的优势,但同时也让你成为整个项目的负责人。
-
路线图:制定一个全面的路线图,概述项目里程碑和功能支持。通过在这上面投入时间,你可以为开发过程提供一个清晰的愿景,并帮助项目保持正轨,同时向利益相关者传达框架的方向。
-
时间线:确定你在框架上投入多少时间。这包括为每个项目阶段设定现实的截止日期,并考虑可能影响时间线的潜在风险和障碍。通过建立一个明确的时间线,你可以确保项目高效地推进,并专注于向目标受众交付价值。
本节中提到的所有考虑因素都可以对你的开发过程做出贡献。花点时间弄清楚所有这些考虑因素的问题,可能会极大地有利于你的项目。许多这些考虑因素将取决于你的框架的问题领域。为了帮助处理这个问题,我们将在下一节中介绍潜在的问题领域。
识别框架问题领域
框架旨在支持一个或多个项目的开发,专注于解决特定的问题领域。我们将问题领域定义为框架准备解决的一系列挑战或问题范围;这是来自图 5**.1的第二支柱。问题领域可以是框架打算使用的特定软件应用程序领域。
正如我们从本书前几章中框架的例子中看到的那样,JavaScript 在客户端和服务器环境中具有广泛的应用。它使我们能够构建满足我们需求和技术的框架。对于你的全新项目,你可能会面临特定的框架类别。通过关注独特项目的技术方面,结合创新功能,你可以使你的项目与野外的现有项目有所不同。
流行的问题领域
在 JavaScript 框架问题领域中,你可以采取以下一些潜在的开发路径:
-
前端框架: 专注于构建前端应用程序框架,你可以找到利用最新浏览器技术来开发独特渲染技术并改进状态管理的方法。鉴于这是一个具有许多现有解决方案的流行框架类别,一个更直接的方法是在这些现有解决方案之上编写自己的抽象——例如,在开发自己的自开发框架接口时,内部使用 Vue.js。这样,你可以针对你的问题空间,专注于其挑战并解决它们,而不是重新发明基础。
-
浏览器解决方案: 与前一点类似,你可以采取构建以浏览器为中心的解决方案的方法,不同于专注于 Web 应用程序开发的框架,并专注于利用浏览器可用的新技术。例如,你可以利用 WebAssembly(webassembly.org)或 WebGPU(w3.org/TR/webgpu)在客户端环境中开发独特的框架。
-
后端应用程序: 如果你正在为后端开发构建一个新的 JavaScript 框架,重要的是要关注可靠性、可扩展性和安全功能。你可以从查看本书或互联网上的示例开始,然后构建一个解决常见后端挑战的框架,例如处理某些类型的数据库、启用不同类型的身份验证和快速 API 开发技术。
-
测试: 如果你正在为 JavaScript 应用程序构建一个新的测试框架,你可以专注于提供一个简单直观的界面来编写和运行测试。你也可以启用对流行的测试框架和库的内置支持,以及与持续集成工具的集成。你还可以提供高级测试功能,如视觉回归测试、独特的并行化和分组技术,以及由模式匹配驱动的测试自动化。在本书的第六章中,我们将开始开发一个简单的测试框架,同时关注学习过程;它是一个很好的简单类型框架的竞争者。
-
原生应用程序: 通过为原生应用程序构建一个新的框架,你可以专注于提供使创建响应式和高性能应用程序变得容易的组件和 API。通常,这些系统提供了对移动和桌面功能的内置支持,例如相机访问、推送通知以及与原生操作系统功能的集成。由于你必须支持的环境数量众多,这种类型的框架开发具有挑战性。然而,正如我们在第一章和第二章中看到的,使用 React Native 和 Electron,这类项目并非不可能。
-
嵌入式解决方案:此类框架将专注于提供简单易用的编程和与硬件设备交互的接口。要创建此类框架之一,你必须为标准传感器和设备开发 API。这包括与外部芯片组、电机、GPS 和蓝牙配件一起工作。此框架背后的主要重点是创建一种独特的方法来减少内存和处理器使用,因为你的目标是嵌入式仪器。尽管过去有许多项目允许运行时与嵌入式设备交互,但 JavaScript 仍然是一个更复杂的挑战。
在前几章中,我们探讨了几个更流行的 JavaScript 框架的例子。然而,JavaScript 的通用性远不止我们所讨论的。这种语言可以使你构建针对其他细分市场的框架项目。JavaScript 在现代网络开发中无处不在,允许开发者构建强大且功能丰富的应用程序。
其他框架路径
JavaScript 允许我们构建许多其他类型的项目;它们都有自己的考虑因素。不分先后,让我们看看更多框架开发路径:
-
游戏开发:由于 JavaScript 是唯一可用的网络浏览器目标运行时,因此它最终成为构建游戏的唯一解决方案。你的 JavaScript 游戏开发框架可以提供工具和实用程序,使构建 Web 和移动平台的 2D 或 3D 游戏变得容易。这些实用程序可能包括内置对物理引擎、动画和音频的支持。在这种情况下,框架可以专注于提供高级功能,如多人支持或虚拟现实集成,因为这些功能变得越来越受欢迎。一些流行的 JavaScript 游戏开发框架包括 Phaser、Pixi.js、PlayCanvas 和 Babylon.js。尽管游戏开发框架对渲染和性能有特定的要求,但你仍然可以使用本书中的知识来构建此类框架。
-
计算:一个新的计算和数据科学框架可能能够执行科学计算和数据分析任务。你可以专注于提供一组用于执行数学运算和处理数据的 API。此框架可能内置了对流行的数据可视化库和统计分析工具的支持。JavaScript 计算框架的潜力在于前端表示和后端计算层。一个框架可以结合这两种可能性。
-
可视化:与计算主题类似,有空间来改进 JavaScript 数据可视化框架。在可视化项目中,你可以专注于提供一套工具和组件,用于创建交互式和动态的可视化。你也可以与可视化库如 D3.js 集成,并探索高级功能,如实时数据流。随着这个领域的不断发展,你可以找到从许多数据源渲染和交互信息的新方法。
-
人工智能:如果你决定开发一个用于人工智能和机器学习(ML)的 JavaScript 框架,你应该优先提供一套构建和训练神经网络的 API。考虑基于广泛使用的 ML API 和库,如 TensorFlow.js (tensorflow.org/js)构建你的框架。这样的框架可能包括与各种类型的 ML 格式和配置的互操作性。
-
用户界面(UI):围绕构建 UI 构建的框架可能对常规前端应用的不同功能集有所帮助。它可以包括可定制组件和响应式 UI 的功能。创新功能可能包括利用现代 CSS 特性的样式和主题组件。该框架可以与外部库如 Tailwind CSS、Material UI、Bootstrap 等集成。如果你在营销或设计相关环境中工作,构建 UI 框架可能是有利的。
根据您的框架目标,这里有一些潜在的框架路径。在本书的前几章中,我们已经详细介绍了这些框架的一些类别的来龙去脉。这并不是在 JavaScript 环境中可能解决方案的详尽列表,但它展示了众多可能性。最受欢迎且最具竞争力的框架类别与构建前端应用相关。
现在我们对问题领域有了更清晰的愿景,在下一节中,我们可以继续考虑框架项目的技术架构。
技术架构
在本章的前几节中,我们确定了我们的利益相关者——那些将直接从我们的框架项目中受益的人。我们还确定了潜在的问题领域。这两个因素为我们想要构建的内容提供了一个坚实的基础。在本节中,我们将探讨图 5**.1的第三个支柱——技术架构——以便我们能够专注于我们计划的项目的技术特性。
抽象层次和灵活性
在 JavaScript 框架的代码 API 中实现实用的抽象层次和灵活性是重要的设计决策。随着你开发框架,这两个原则是确保框架可用性、可维护性和适应性的必要条件。
如在第二章中探讨的,合理的抽象级别对于为开发者提供干净、易于理解的接口至关重要。底层实现的封装复杂性提高了生产力,并最小化了错误的风险,因为开发者使用的是更高层次、更直观的 API,这使他们免受不必要的复杂性。
合理的抽象级别促进了代码的模块化和可重用性,因为框架的功能可以更容易地连接和适应不同的环境。提供一定程度的模块化使开发者能够构建在现有模块之上,培养一个由开发者驱动的扩展生态系统,进一步增强了框架的功能。通过在抽象和灵活性之间取得正确的平衡,JavaScript 框架可以满足各种项目,从小型到复杂的应用。
在代码 API 中提供灵活性是成功 JavaScript 框架的另一个关键方面。灵活的 API 能够适应不同的编码风格、范式和用例,使开发者能够根据他们的独特需求调整他们的方法。这种适应性在快速发展的网络开发世界中至关重要,因为新的工具、库和模式不断涌现。通过提供多功能的 API,JavaScript 框架可以在这些不断变化的趋势面前保持相关性和价值。
抽象的一个潜在陷阱是创建高度有偏见的抽象,这会严格限制开发者如何使用框架。虽然抽象可以简化特定的用例,但它们可能会阻碍框架的整体灵活性,并限制其适用于更广泛项目的适用性。如果您想构建一个不那么有偏见的框架,考虑为您的利益相关者提供可扩展的选项,例如使用不同的模板引擎或在不同构建的应用程序中管理状态的不同方式。
在抽象和灵活性之间取得正确的平衡,并避免过度有偏见的抽象,将帮助您打造一个多才多艺且持久的 JavaScript 框架。
环境兼容性
JavaScript 在各种环境中运行,包括浏览器、服务器、移动设备以及其他独特的硬件,每种硬件都有其独特的特性,这使得兼容性成为任何框架成功的关键因素。确定您框架的运行环境兼容性就是确定要支持和维护哪些运行时。通常,在 JavaScript 框架中,这涉及到前端和后端功能的时间和技术的投资选择。这包括特定浏览器的 API 和与不同后端系统的兼容性。除了前端和后端系统之外,JavaScript 还支持许多其他环境。
框架开发者面临着一个重大挑战,即确保与多个 JavaScript 环境及其特定怪癖的兼容性。从高层次来看,这包括不同类型的浏览器引擎和与不同模块系统的兼容性。这项任务需要仔细考虑和设计决策,以确保框架能够在所有目标运行时无缝工作。
第一个设计决策是关于配置适当的 JavaScript 环境的兼容性。开发者必须考虑框架的目标环境,并确保它与所选设置兼容。例如,如果框架是为 Web 应用程序设计的,开发者必须确保它能够在多个浏览器版本和 API 之间无缝工作。由于浏览器功能的不同,可能会出现不兼容性,导致渲染不一致或应用程序无响应的问题。
在开发新的 JavaScript 框架时,另一个重要的考虑因素是处理环境差异。编写额外的代码兼容层对于微小的和重大的运行时差异来说是有价值的。处理运行时差异包括为旧版浏览器和旧版服务器端运行时投入时间以实现向后兼容。一般来说,支持多个前端 JavaScript 环境需要同一浏览器环境的不同版本。例如,许多浏览器,如 Firefox,都有各种版本,每个版本可能都有独特的功能或特性。开发者必须确保框架能够处理这些差异,并且无论浏览器版本如何,都能提供最佳的性能和功能。
例如,你必须处理跨运行时兼容性,以便启用服务器端渲染或 Node.js 测试前端组件。在构建框架时,JavaScript 服务器环境可能需要特定的考虑。它们可能具有与浏览器不同的 API,并且某些功能,如 DOM,可能不可用。因此,开发者必须确保框架能够处理这些差异,并在服务器环境中提供最佳性能。框架开发者包括 JavaScript polyfills 和类似的代码片段,为其他环境中的新功能和缺失功能提供回退机制。在构建一个应在多个领域工作的新框架时,这些是必不可少的。
确保与多个 JavaScript 环境兼容需要在框架开发和维护期间进行额外的彻底测试。测试在早期开发周期中识别和解决兼容性问题至关重要。例如,我们可以使用上一章中看到的自动化测试工具来测试框架在不同浏览器版本和移动设备上的兼容性问题。包括这些测试有助于确保框架在所有目标环境中提供最佳的性能和功能。然而,测试所有可能的运行时用例和特性可能具有挑战性,并且对所有框架将使用的配置进行测试是不可能的。幸运的是,随着 JavaScript 运行时的成熟,兼容性问题显著减少。如果你正在开发一个浏览器外的框架,类似于 Electron 或 React Native,你将面临更多的挑战。你必须确保框架与为你的项目设计的多个操作系统兼容。例如,操作系统运行时可能具有不同的功能,这会影响框架的功能集。
总的来说,你能够定义支持的 JavaScript 环境并控制你在框架中支持的运行时类型,知道与多个 JavaScript 环境的兼容性需要对你项目的持续维护和更新。这种维护包括与新浏览器版本或服务器环境兼容,这些环境可能会调整其功能或添加新功能。
利用库
在开发新框架之前承诺特定的 JavaScript 库是有意义的。使用现有的 JavaScript 库可以节省你的时间——你可以用这些时间来专注于框架的功能和技术架构。框架通常依赖于库来构建内部结构。这些库通常在幕后间接启用框架功能集,包括数据管理、路由、与 DOM 交互以及抽象化 JavaScript 运行时复杂性等功能。随着框架覆盖更广泛的功能集并塑造开发体验,内部库专注于为特定问题提供精确的解决方案。
选择正确的库集可以显著影响开发过程和你的框架的形状。你在框架中使用的库可能会让你成为它们的专家用户。然而,平衡使用库的好处与潜在的缺点,如兼容性问题、API 限制和持续维护,是必要的。
在探索其他 JavaScript 框架时,我们可以识别它们为特定功能所依赖的库。根据架构,您的框架可以将库直接集成到框架中,或者使用它来扩展框架的某些方面。如果我们看看 Angular,我们会发现它使用了 BookService 服务:
import { HttpClient } from '@angular/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map, retry } from 'rxjs/operators';
@Injectable()
export class BookService {
private baseUrl = '...';
constructor(private http: HttpClient) {}
getWeather(latitude: number, longitude: number):
Observable<any> {
const url = `${this.baseUrl}?latitude=${latitude}
&longitude=${longitude}¤t_weather=true`;
return this.httpClient.get(url).pipe(retry(3));
}}
之前代码使用 Observable 类首先返回 getWeather 函数。在您的 Angular 类中,您可以依赖 RxJS 提供的许多数据处理操作符。此外,RxJS 库还提供了错误处理操作符,如前述代码中的 retry 调用所示。关于库操作符的详细解释可以在 rxjs.dev/guide/operators 找到。
探索 RxJS 示例
书籍仓库中的 chapter5 目录包含了一个使用 Angular 和 RxJS 库的示例。您可以通过在章节目录中运行交互式脚本或在 angular-rxjs 目录中执行 npm install,然后运行 npm run dev 来在自己的计算机上尝试这个示例。
示例应用程序将使用之前提供的 BookService 服务来获取数据。API 数据包含额外的属性,您可以使用这些属性来扩展现有应用程序。有关更多信息,请参阅 README.md 文件。
在另一个库使用的例子中,Vue.js 最初使用 Vuex 作为集中式状态管理的库。然而,随着框架的发展,管理状态的方法发生了变化。Vue 已经转向推荐并使用 Pinia (pinia.vuejs.org) 进行状态管理。基于 Flux 架构的直观方法,这个与 Vue 密切相关的库允许开发者使用多个存储来管理状态,增强了可扩展性,并且与框架的功能更加紧密地一致。另一个我们在 第一章 中看到的例子是 Next.js,它使用 React 库进行渲染和其他功能。Next.js 专注于使用 React 提供的原始功能来抽象出直接使用库时的复杂性。
当您将库引入到框架中时,要明智地选择它们。通常,对于框架的用户来说,抽象出对库的直接访问更为轻松。否则,您必须在框架中支持特定的库 API,这会将您锁定在特定的编码模式中。历史上,Ember.js 必须投入精力将框架与其对 jQuery 库的使用解耦。这种迁移意味着为试图跟上框架最新版本的项目提供更新路径。
随着您的框架发展,您将在 JavaScript 库的生态系统中发现巨大的好处。挑战将是随着目标运行时的演变,跟上这些项目的不断发展。
编译器工具
在第三章和第四章中,我们考察了框架开发模式的一些实例。这些模式包括使用编译器和其它构建工具来开发框架结构和结构。毫无疑问,这些模式中使用的工具使得开发、重构和维护工作流程变得更加易于管理。因此,除非你的框架有特定的理由避免这些工具的好处,否则强烈建议充分利用生态系统。
构建工具和编译器
在编写框架项目的代码时,你希望从所做的代码更改中获得良好的反馈循环。这个反馈循环可以来自运行项目测试或在你开发新功能或修复错误时使用你的框架的示例应用程序。这种迭代工作流程可以通过内置的 JavaScript 行为进行配置,或者你可以依赖许多现有的构建工具和编译器。对于开发过程,编译器工具的选择可以显著影响并影响你框架的开发效率。回顾框架展示,我们看到了使用工具如 Rollup.js、webpack和esbuild进行 Web 框架开发和打包的例子。
这些工具的选择将取决于你框架的精确需求。在仔细选择使用这些工具时,你需要确保评估它们的优缺点。此外,你选择的工具应该适合开发工作流程和良好的框架发布工作流程。你可以决定将这两个工作流程分开,但这样可能会导致你需要维护的工具过多。例如,我们可以考虑以下因素来使用这些工具:
-
整体 JavaScript 运行时和功能支持,包括广泛的 JavaScript 模块格式支持和高级功能,如摇树优化和智能代码打包
-
对于前端系统,评估浏览器和 Web API 的支持
-
针对不同工作流程和环境时的配置复杂性和灵活性,可能选择零配置工具与全面配置工具
-
框架开发和生产构建的构建时间速度
-
与其他类似解决方案相比,工具的成熟度
-
开发者功能集,例如热模块替换(HMR)、开发服务器和即时实时重新加载
-
与外部工具的集成,如测试框架
这些因素可能因你的框架问题空间的不同而有所不同——例如,前端与后端领域。
尝试使用 esbuild
书籍仓库中的chapter5/esbuild目录包含一个使用esbuild打包前端文件的示例项目。你可以参考项目目录中的build.js文件以获取esbuild编译器的配置。当你在本机上本地运行此项目时,该工具将从src目录中提取资源,并将生成的文件输出到dist目录;这些文件随后被加载到项目根目录下的index.html文件中。构建步骤是通过在项目目录中执行npm run dev命令来执行的。
投入足够的时间,你可以开发出自己的编译器或打包工具。我们已经看到了使用 Svelte 等项目进行自定义框架编译器开发的先例。创建自己的工具是一项更大的任务,但这可以使你的框架与众不同,并具有巨大的潜力。
JavaScript 扩展
在设计决策部分,特别提到了扩展 JavaScript 功能的 TypeScript 和类似工具。这些 JavaScript 语言扩展在近年来一直是框架开发的核心理念。即使使用 TypeScript 在框架开发工作流程中的流行度可能会随着时间的推移而下降,但它很可能会被其他类似的工具所取代,这些工具包含了 JavaScript 本身无法直接提供的优势。特别是 TypeScript,框架开发者可以从额外的功能中获得生产力的提升,如静态类型、接口、装饰器、命名空间等。所有这些都非常有利于框架开发。
假设你不确定是否要将额外的 TypeScript 工作流程引入到你的框架中,或者你有一个与 TypeScript 工具冲突的特定 JavaScript 环境。在这种情况下,你可以考虑一个设计决策,选择使用 TypeScript 类型的 JSDoc 注释版本。可以在typescriptlang.org/docs/handbook/jsdoc-supported-types.html找到支持 JavaScript 文件带有 TypeScript 注释的类型范围。如果你不介意额外的转换步骤,并且完全选择 TypeScript 生态系统,那么它可以帮助你克服许多开发难题,例如以下内容:
-
减少在运行时识别出的代码问题数量
-
加快代码重构的速度:在框架中,这种能力更为关键,因为框架的代码库比常规的 Web 应用程序项目更加动态。
-
改进基于类的编程概念:你可以使用额外的构建块,如接口、继承特性等,以拥有一个设计良好的代码库。
-
拥有一个描述性更强、文档更完善的代码库:这会主动地对你和其他与你一起工作的团队成员带来好处。
-
允许你更快地利用新的语法特性:TypeScript 不断添加新的有价值的功能,并且不受浏览器缓慢采用新语法特性的限制。
所有这些好处都非常实用,最终使用 TypeScript 或类似 TypeScript 的解决方案来提升你的编码体验是一个良好的设计决策。
摘要
本章的核心思想探讨了我们在开始新项目时需要研究和牢记的几个关键因素和考虑事项。首先是确定框架的利益相关者和目标,这些包括实现这些目标的目标和受众。然后,我们考察了潜在的问题领域,重点关注理解我们可以考虑的项目类型。最后,我们探讨了可能塑造我们项目的特定 JavaScript 架构设计决策的例子。
考虑所有这些信息将帮助你创建一个更好的框架项目。同时,我们也将在这本书的整个过程中使用这些框架考虑因素。我们将在下一章开始应用所有这些考虑因素,因为我们将从零开始构建一个新的框架。
第六章:通过示例构建框架
本章结合了书籍第一部分和第二部分的所有见解和架构知识,并将其付诸实践。随着我们开发一个基于迄今为止所见模式和最佳技术的简单 JavaScript 测试框架,请跟随我们的步伐。这种实用方法将使我们能够通过示例学习,这对于此类软件主题来说是一种很好的教育方法。我们在这里构建的框架是一个专门为本章开发的新项目。我们可以将这个新的示例框架项目视为 JavaScript 框架开发的“Hello World”练习。通过这个练习,我们的目标是训练我们的能力,并将其应用于后续的实际项目中。
本章将通过示例介绍构建框架的以下主题:
-
首先,我们将为新的框架项目制定初始方法,包括确定目标、利益相关者和品牌,以从头开始创建某些内容。这主要涉及将第五章中关于项目考虑因素的学习付诸实践。
-
接下来,我们将学习如何概述一个典型的初始架构设计,以便将我们的测试框架实现启动。这包括概述组件如何组合以及项目的独特特性和接口。此外,我们还将总结开发人员期望利用我们的项目时所需的公共接口。
-
最后,我们将基于创建的设计实现我们的测试框架,包括核心功能组件、命令行实现、浏览器测试集成等。
技术要求
实现的框架代码位于github.com/PacktPublishing/Building-Your-Own-JavaScript-Framework的书籍仓库中。我们的示例框架代码包含在chapter6目录中。为了跟随本章的实现说明,建议您与章节目录中的框架文件一起进行操作。
要运行代码,请确保已安装Node.js v20 或更高版本(您可以从nodejs.org下载)。为了更容易地管理不同版本的 Node.js,您可以使用 Node.js 版本管理工具,例如nvm(github.com/nvm-sh/nvm)。运行nvm将从框架项目目录自动安装适当的 Node.js 版本,因为项目已针对特定版本进行配置和测试。请确保运行npm install以获取项目的依赖项。运行npm test应输出一系列通过测试。这意味着您的本地配置已经全部正确设置。如果您从chapter6目录运行npm start,交互式脚本将为您执行所有这些步骤。
要测试此示例框架的前端部分,请使用基于 Chromium 的最新浏览器版本或 Firefox 的更新版本。要调试框架,请参考第二章中的关于调试部分。您可以使用该章节中记录的方法。
确定目标和利益相关者以及框架品牌
我们框架实践练习的主题是一个新的 JavaScript 代码测试框架。
目标
我们将努力实现的目标是为不同复杂度的项目提供稳健的测试工具。测试框架的主要目标是向开发者提供一个可靠、快速且多功能的平台,以验证其代码的功能性、性能和稳定性。此外,它们旨在最大限度地减少潜在错误的风险,并创造一个无缝的开发体验,最终产生高质量的产品。
为了实现这些项目目标,我们的 JavaScript 框架将专注于快速准确地执行测试,支持各种报告输出格式,并营造一个对开发者友好的环境。通过优先考虑易用性和与其他 JavaScript 工具和应用框架的集成,我们力求使测试过程尽可能无缝和高效。该框架还将支持跨平台测试,包括 Node.js 和真实网络浏览器环境,确保代码在各种环境中表现一致并符合预期。
此项目的附带条件之一是为您,即读者,创建一个类似沙盒的环境,通过提供一个可扩展且更直观的项目,您可以从中学习。我们鼓励您运行代码并扮演潜在框架贡献者的角色。
利益相关者
新的 JavaScript 代码测试框架的利益相关者可能包括各种具有特定软件测试需求的软件工程师和网络开发者。首先,JavaScript 开发者可以在他们的面向用户的项目中使用此框架,并将其包含在其他 JavaScript 软件的开发中。此框架可以是多环境测试代码的全栈解决方案。在开发者社区中,我们正在努力开发一个适合前端、后端、全栈开发者、质量保证和 DevOps 工程师,以及最终其他框架/库作者的框架。
我们的项目利益相关者可以从这个项目的架构特性中受益。例如,如果我们正在真实浏览器环境中测试代码,那么开发的框架可能对前端工程师来说是一个很好的卖点。通过构建这个项目,我们选择的 JavaScript 测试功能集可能会与某些工程工作流程产生摩擦或不兼容,例如在较旧的运行时和浏览器中运行测试。因此,主要来说,我们针对的是那些能够生活在新功能的前沿并且能够轻松地将他们的代码库与最新环境保持同步的开发者子集。
如果我们是在内部开发一个 JavaScript 代码测试框架,我们的利益相关者将是初创公司或大公司的内部开发者。在这种情况下,我们可以收集有关内部团队中最需要的功能集的见解。此外,可以开发特定的功能来满足特定的组织需求。项目的迭代和稳定性将取决于它在内部的使用情况。
利益相关者和框架开发者都能从一致的项目沟通和身份认同中受益。这就是我们的新框架可以通过添加一些品牌标识来获得一些好处的地方。更多内容将在下一节中介绍。
框架品牌
在深入架构和编码之前,花一些时间在我们的项目品牌和身份上,对项目结构来说是有益的。围绕框架创建一个身份是内部和公共项目中的一种常见模式,因为它清楚地定义了项目的逻辑边界,并为项目提供了总体背景。创建一个身份可能只是为你的新项目选择一个临时名称那么简单。它也可以是一个更复杂的创造性过程,正如我们建立项目名称时那样。这个名称将有助于为我们的项目创建一个命名空间,并由代码库以及加载此新项目的测试/依赖项目内部使用。当然,名称可以在稍后通过一些查找和替换来更改,所以这个决定不必是最终的。
我们将为我们的新项目使用一个新的框架名称,并在第七章和第八章中将其用于后续项目的进步。我们将Componium定义为我们在本书中开发的框架的总品牌。这个名字是对单词Component的戏谑,结合了-nium结尾,这使得这个名字听起来具有技术和软件感。此外,这个名字并不完全独特,因为 1821 年的一种乐器与我们的框架共享了同一个名字:mim.be/en/collection-piece/componium。更重要的是,这对我们来说不是问题,因为在 JavaScript 生态系统中没有这个名字的冲突。选择一个命名空间必须伴随着一些独特性,避免与现有的开发者工具发生命名冲突。
继续创意过程,在 图 6**.1 中,你可以看到我们框架的标志和配色方案,这些可以在文档中使用,并在项目的视觉特征中展示:

图 6.1:新框架名称和标志
测试框架将特别称为 Componium Test。添加身份资产有助于使项目更具辨识度,并在开发者社区中建立更强的品牌影响力,培养信任和认可。
在更技术性的方面,为你的项目创建一个命名空间,就像我们在这里用 componium 做的那样,可以在多个场合提供帮助。命名空间消除了潜在的命名冲突,并有助于与其他框架的冲突,同时帮助我们逻辑上对代码库及其组件进行分组,这对内部和外部开发者都有帮助。根据框架项目的类型,创建命名空间还可以使代码库的模块化方法更加完善。我们在 第二章 中看到了一个例子,Vue.js 采用 vue-* 命名空间来命名其许多包。此外,命名空间还可以帮助促进框架的定制和扩展,例如其他开发者贡献插件和补充。
现在我们对为我们的项目创建身份有了更多的了解,并且我们的品牌练习已经完成,我们可以转向构建我们新框架的激动人心的部分。
构建新框架
现在,我们将进入我们实用方法的架构部分。到目前为止,我们知道我们在构建什么,以及我们为谁构建它。现在是时候确定我们的功能集的形状,并看看我们如何为我们的用户启用这些功能。我们希望覆盖的最基本用例是生成 JavaScript 代码的断言,如这里所示:
// basic.js
export function helloReader() {
return "Hello reader!";
}
// tests/basic.js
import ct, { assert } from "componium-test";
import { helloReader } from "../basic.js";
ct({
basic: function () {
assert.strictEqual(helloReader(), "Hello reader!",
"output is correct");
},
});
以下代码示例测试了 helloReader() 函数,并验证返回了正确的字符串。从这些基础知识开始,为了更进一步,我们花费时间在核心功能上,并首先确定超出范围的特性。稍后,这可以帮助推动技术决策,当我们头脑风暴适合我们项目的扩展功能集时。我们的策略包括比较现有测试工具提供的功能,开发独特的特性,并思考哪些功能超出了初始范围。
为了进行一些功能比较,我们可以参考第一章中的一些框架。Jest和Vitest是值得考虑的框架,因为它们与我们本章构建的框架相似。此外,生态系统中的其他项目还包括jasmine、ava、tap、tape和mocha。这些项目中的大多数都提供了一个高级测试接口,具有特定的测试生命周期和创建断言的不同方式。所有这些现有的开源项目还提供了一套常见的核心功能,例如在各个项目中运行框架的可执行性、不同的输出格式选项、对接口进行存根或间谍的能力,等等。
在接下来的小节中,我们将探讨一些可以作为我们针对Componium Test项目初始方法的一部分实现的一些独特功能。
选择功能
为测试框架构思一系列功能可能既令人疲惫又令人兴奋。在测试工具开发方面,需要涵盖的领域非常多,大部分功能集覆盖了不同的开发领域。在本节开头提到的架构示例中,我们列出了基本的测试用例,现在我们可以在此基础上进行扩展。以下是一些可能有助于我们的项目为其利益相关者提供更完善功能集的附加功能类型:
-
强大的测试运行器:该框架提供了一个具有执行单个或完整测试套件能力的测试运行器可执行文件。它为用户提供了一种选择性的执行格式,可以指定要运行的测试,这在开发和调试情况下非常有用。测试运行器还允许用户使用特定的语法定义测试用例。
-
跨平台测试:该框架支持在不同的 JavaScript 环境中进行测试,例如提供对 Node.js 和 Web API 测试的一些支持。
-
断言类型:用户可以使用不同类型的断言样式,这为测试的结构化提供了灵活的选项。这些不同的断言样式满足了不同的偏好,并可能影响测试代码的可读性和可维护性。提供多种断言类型允许开发者选择最适合他们需求和编码风格的选项。
-
测试套件接口:提供一个丰富的接口,包括设置函数和生命周期钩子。这些功能对于有效地组织和管理工作是必要的。一个良好的测试套件接口允许在测试或测试套件之前和之后执行必要的操作,从而实现更结构化的测试方法。
-
代码桩功能:启用对现有代码的监视、桩化和模拟的能力,以允许实现替换。这是确定应用程序代码所有可能调用方式的关键特性。例如,Jest 框架内置了以下功能:jestjs.io/docs/jest-object#mock-functions。
-
代码覆盖率:输出代码覆盖率报告,显示被测试覆盖的代码库的百分比。
-
webpack、rollup.js以及其他。 -
使用
.表示通过测试或使用F表示失败。通常,这些报告类型可以在运行测试框架可执行文件时通过环境或标志来指定。 -
插件接口:公开一个插件接口,使其能够扩展框架的功能集。对于测试框架来说,这可能是一个提供额外报告者或替换内置库的选项。
除了这个特性列表之外,我们的框架还可以专注于改善围绕 JavaScript 特性的开发者体验,例如处理 ES 模块环境或测试中的异步行为。它还可以选择满足许多特定的 JavaScript 使用场景,例如能够与特定库一起工作,并测试媒体和图形相关功能。
我们现在有一份丰富的可想象特性列表,这将为我们框架的实现提供一个合理的基石。特性规划和开发过程的一部分也包括确定哪些功能将超出范围。这就是我们在下一节中简要介绍的内容。
确定超出范围的功能
尽管我们已经确定了一个丰富的功能集,但它并没有涵盖测试框架所有潜在的使用场景。为了进一步提高项目的易用性,后续版本中还需要开发一些组件。特别是,这个初始版本旨在为框架创建一个基础,使我们能够快速迭代并添加新功能。专注于创建一个持续和直观的开发环境,而不是立即匆忙添加大量功能,会更好。在实际应用场景中,作为一个框架开发者,在迭代项目时收集用户反馈并解决利益相关者的最紧迫需求是非常重要的。
下面是一些可以作为后续版本潜在添加的功能和组件:
-
测试监视器:提供一个测试运行器模式,该模式监视文件更改,并在底层组件更改时重新运行测试。这对于开发者来说可能是一个受欢迎的功能,因为它在开发过程中提供了实时反馈。
-
时钟和日期操作:在现有模拟功能的基础上进行改进,JavaScript 项目中内置的时钟和日期模拟可以使冻结或操作日期变得更加容易。提供这个接口进一步提高了易用性。
-
快照测试:涉及捕获和比较复杂组件的输出。通过简化断言方法和将预期输出源与测试文件结构分离,可以节省时间。
-
重试:能够重试失败的测试可以改善不同测试环境中的用户体验。这个功能需要谨慎处理,以确保只有预期的失败才会重试,并且测试运行者不会报告已损坏测试的通过结果。
对于这样的框架,肯定还有更多可以开发的组件,但我们已经识别出的功能集合足以让我们构建架构设计大纲。开发过程的下一部分是从功能集中挑选出最重要的部分,并设计一个架构来实现这一功能。
设计大纲
我们需要引入一个可执行接口,以启用具有跨平台测试行为的测试运行者功能。这个接口作为命令行工具实现,必须处理用户在不同环境中的运行选项。最后,为了实现我们的主要目标——断言代码行为,执行模式必须返回测试断言的状态。
当可执行文件触发测试时,测试将发出其状态和事件。运行者需要尊重这些测试的生命周期,并订阅与测试工作流程相关的事件。可能的事件包括通过或失败状态,或者在出现断言错误时向上冒泡。
创建一个如图所示的设计图,可以更容易地看到使功能集能够相互交互的组件:

图 6.2:初始设计图
在图 6.2中,执行流程从左侧图中定义的可执行文件或命令行开始。在这种情况下,可执行文件是ct命令,代表Componium Test。如果开发者使用命令行作为入口点,那么CLI 处理器会解析提供的标志和选项,确定测试是在 Node.js 环境还是网页浏览器环境中运行。这将使我们能够在功能规划过程中评估的跨平台测试能力。
在 Node.js 环境中,NodeExecutor类可以解析提供的测试文件并运行其断言,记录执行的真实状态并输出测试的最终结果。chapter6/componium-test/packages/node/executor.js文件包含NodeExecutor的内容。代码被编程为为每个测试套件生成工作环境,并在测试用例中运行断言。随后,收集通过和失败的测试摘要。
在浏览器上下文中,BrowserExecutor 类依赖于由 puppeteer 库提供的外部网络浏览器桥接器。它创建测试运行器和真实浏览器环境之间的通信通道,排队预期的测试套件,并使用类似的接口捕获测试断言和生命周期输出。浏览器环境还有不同的运行方法,例如 无头 和 有头 模式,在测试运行时浏览器对用户是隐藏还是显示。此功能需要集成库和测试运行器可执行文件。此执行器位于 chapter6/componium-test/packages/browser/executor.js。此代码与 Node.js 方法类似,但由于它在浏览器上下文中运行,它必须将测试套件的结果发送回在终端进程中运行的 Node.js 测试运行器。环境差异使得这两个执行器的技术设计大相径庭。
图 6**.2 中的图表还概述了直接通过 node CLI 运行测试作为脚本文件的支持。在这种情况下,测试绕过 ct 接口,并在脚本运行时直接输出结果。这是一个方便的特性,支持直接测试工作流程。
图 6**.2 中的概述和图表没有捕捉到测试运行器工作流程的全部细节,但它们对我们的开发过程很有帮助。当你开发自己的框架时,你会发现创建这样的工件可以帮助你在构建不同类型的软件解决方案时更加高效和精确。有了概述的架构和功能,我们可以继续到接口设计过程的下一部分。
设计接口和 API
我们与框架交互的两个主要接口是主框架模块和测试运行器可执行文件。开发者必须通过 API 文档、示例等熟悉这些接口。此外,对这些 API 功能的破坏性更改或修改将影响已集成到框架中的项目。
我们正在查看的第一个接口是导入的 componium-test 框架模块。此模块导出主测试对象,该对象可以通过 JavaScript 对象表示法 (JSON) 接受测试函数。该模块还导出其他框架接口,例如断言和模拟库。以下是如何使用此模块的示例:
代码片段 1
import framework interfaces
import ct, { assert, fake, replace } from "componium-test";
import Calculator from "../fixtures/calculator.js";
// fixture to test
let calc;
ct({
describe: "Calculator Tests",
beforeEach: () => {
calc = new Calculator();
},
multiply: function () {
assert.equal(calc.multiply(3, 2), 6, "3 * 2 is 6");
},
mockMultiply: function () {
const myFake = fake.returns(42);
replace(calc, "multiply", myFake);
assert.strictEqual(calc.multiply(1, 1), 42,
"fake interface is working");
},
afterEach: () => {
console.log("called afterEach");
}
});
使用模块在测试文件中的代码示例可以在框架的 tests/calc-tests.js 路径中找到。在这段代码中,我们看到包含了一个示例 Calculator 脚本的基本断言;这些是通过导入 assert 函数实现的。根据预期的功能集,我们有测试生命周期方法,如 beforeEach。此外,通过 fake 和 replace 函数启用了替换 multiply 返回点的模拟功能。
第二个接口是当用户安装框架时提供的命令行实用程序。这是一个测试运行器,允许与大量测试套件交互:
> ct --help
ct [<tests>...]
Options:
--version Show version number [boolean]
-b, --browser Run the test in a web browser [boolean]
--keepAlive Keep the web browser alive to debug tests [boolean]
--help Show help [boolean]
可执行文件支持多种选项以启用预期的功能集。此外,--help 标志还可以显示可用的命令及其快捷键。ct 可执行文件接受一个或多个测试套件作为参数,通过 ct [<tests>...] 语法可见。开发者在从 JavaScript 的 npm 包注册表中安装框架后应期望存在 ct 可执行文件。
我们现在了解这两个开发者级别的 API——测试可执行文件和测试接口。现在我们可以继续实现那些两个框架功能背后的功能特性。
实现新的框架
实施过程进一步指导我们如何创建 JavaScript 框架。在这个实施过程中,我们将看到测试运行器的不同入口点以及可区分的特性。本节大致遵循 图 6**.2 中的图,其中我们概述了框架设计。现在我们知道了我们的功能和项目架构的大致想法,让我们广泛概述这个项目的内部实施步骤,如下:
-
确定设计的测试套件配置如何在 Node.js 环境中实现。
-
创建内部包以在不同的环境中执行测试套件。
-
实现真实网页浏览器模式测试的基础设施。
-
从测试运行器收集结果并将其输出给用户。
按照第 1 到 4 步,我们开始实施,首先检查我们的测试是如何构建和执行的。在先前的 Calculator Tests 片段中,我们可以检查 JavaScript 对象结构,包括一个 ct 函数调用,接受一个测试套件对象。一些特殊属性包括描述测试套件名称和运行生命周期方法,如 before 和 after。这些方法的执行生命周期可以在以下图中看到:

图 6.3:测试生命周期
要启用ct函数调用,测试套件可以访问componium-test包以及我们框架的公共接口,例如断言库功能。我们可以在代码片段 1中看到将框架包含在calc-tests.js代码块中的示例。除了导入框架功能外,测试文件还导入了我们要测试的代码。
在 Node.js 环境中导入我们的框架需要将其正确导出作为包的一部分。为此,我们在package.json文件中定义一个exports属性。这允许项目将Componium Test配置为依赖项,通过名称导入模块:
代码片段 2
"exports": {
".": "./packages/runner/tester.js",
"./package.json": "./package.json"
},
上述配置指向我们的tester.js文件,该文件处理大多数测试运行逻辑,它由我们的runner包引用。
一旦测试对象被加载到框架中,它将被引导到名为packages/runner/tester.js的文件。tester.js文件是大多数测试发生的地方,包括在适当时间执行的生命周期方法,以及主测试函数。由于测试套件可以包含许多测试,测试器将继续调用所有提供的函数。为了测量测试的速度,我们可以依赖performance API(developer.mozilla.org/en-US/docs/Web/API/Performance/now)。这些 API 提供了高分辨率的时间戳,为我们提供了关于测试运行所需时间的数据。
测试功能
通过详细的测试断言实现卓越的测试体验,我们的项目需要一个可以在新编写的测试中使用的断言库。断言接口应包括相等比较、值大小比较、对象值评估等。
为了专注于广泛的框架开发,我们将导入一个名为should、expect和assert的外部测试断言库。随后,我们可以开发自己的断言基础设施或用不同的库替换 Chai,我们可能可以自己开发这个库。
当测试脚本执行我们提供的测试时,如果任何比较函数失败,我们方便的断言库将抛出异常,如下所示:
AssertionError: 3 * 2 is 6: expected 1.5 to equal 6
at multiply (file:///Users/componium-test/tests/
calc-tests.js:20:12)
at ComponiumTest.test (file:///Users/
componium-test/packages/runner/tester.js:92:15) {
showDiff: true,
actual: 1.5,
expected: 6,
operator: 'strictEqual'
}
上述输出是测试中失败的片段之一,其中导入的Calculator模块未能正确执行multiply操作。框架需要处理这种代码行为。在断言或其他错误的情况下,测试运行器需要至少向运行进程发出至少一个或多个失败测试的信号。如果至少有某个东西翻转了errorOccurred标志,那么它将导致我们的测试节点进程以失败状态退出。
除了断言库之外,我们希望在开发的框架中提供模拟和存根功能。使用类似的模式,我们包括 Chai.js 库并将其作为测试 API 接口暴露,我们将包括Sinon.js (sinonjs.org)库,它具有创建测试存根、间谍和模拟的丰富接口。它非常适合我们的项目,因为它支持许多 JavaScript 环境,并且在其他项目中经过多年的实战考验。这个库的存在将使开发者更容易增加代码覆盖率并编写更有效的测试。
在示例测试文件的代码片段(代码片段 1)中,我们使用 Sinon.js 的函数创建一个假的mockMultiply返回点。Sinon.js 方法也被从直接使用中抽象出来。相反,它们作为componium-test包的一部分被暴露。用户可以通过框架的packages/mock/lib.js模拟包访问库的模拟、伪造和存根机制。
通过简单界面暴露强大的断言和模拟库,我们的框架可以从一开始就获得丰富的功能。
创建命令行工具
在前面的章节中,我们讨论了将我们的tester.js文件作为导入的入口点来运行测试。为了匹配预期的功能集,框架需要支持从给定目录运行多组测试。这意味着以批量方式支持给定格式的多个测试套件。这意味着测试套件的数量将以以下格式组织:
import ct, { assert } from "componium-test";
ct({...});
要获得这个批量执行功能,我们需要为我们的框架创建一个命令行工具。为了与 Node.js 命令行环境集成,我们需要在package.json文件中提供一个bin键来导出命令行文件,如下所示:
"bin": {
"ct": "bin/ct.js"
},
我们导出的二进制快捷键只是ct,以便从其他项目中轻松执行框架命令。为了开发二进制文件,我们可以依赖yargs库 (yargs.js.org)来处理用户输入,解析从process.argv变量提供的进程参数。有关可执行文件结构的详细信息,请参阅packages/bin/ct.js文件。在可执行文件的结构中有几个需要记住的功能,重点是开发良好的接口并启用框架的特定功能,如下所示:
-
使用
#!/usr/bin/env node将解释器包含在文件的第一个行中。这会向系统 shell 发出信号,使用 node 可执行文件来处理此文件。这被称为Shebang (wikipedia.org/wiki/Shebang_%28Unix%29),其中在脚本中提供了解释器可执行文件。 -
将二进制包逻辑集中在与解析命令行标志和确保良好的命令行界面体验相关的代码上。这包括支持
--help标志,提供快捷方式到标志,以及测试可执行文件的使用边缘情况。命令行界面(CLI)的标志或选项应遵循双短横线(--)结构或单个短横线(-)结构。 -
可执行文件应遵循 CLI 的标准规则。CLI 进程应以适当的退出状态码退出,并指示进程是否失败。这对于我们在本章中使用的测试运行器尤为重要。
根据执行模式,Node.js 或浏览器,CLI 文件(packages/runner/cli.js)选择正确的执行器类,并向该类提供一个目标文件列表。NodeExecutor 和 BrowserExecutor 类负责处理所有测试文件,并评估这些文件中的任何测试是否失败任何断言。在 Node.js 测试环境中,我们使用 NodeExecutor 类来运行测试。该类会启动新的工作线程以并发执行测试套件。此接口的主要目的是为多个目标文件运行测试,并返回一个总体通过或失败的结果。
浏览器测试工作流程
我们在开发的框架中探索的另一个功能是在真实浏览器环境中执行测试的能力。为此,CLI 接受一个 –browser 标志,该标志切换测试框架的运行模式。对于开发者来说,此接口的入口点可能看起来像这样:ct –browser test/some_test.js。根据框架可执行文件的方法,我们还可以引入一个单独的 ct-browser 可执行文件,直接在浏览器上下文中执行测试,而无需担心额外的参数。为了实现此功能,框架依赖于 puppeteer 库来启动一个新的浏览器实例,并与它建立通信通道。
图 6.4 展示了浏览器测试运行器工作流程的调试视图:

图 6.4:浏览器测试运行器工作流程的调试视图
这种操作模式有助于在浏览器测试失败时访问测试。此环境允许开发者在其代码中设置断点并调试测试。packages/browser/executor.js 中的 BrowserExecutor 文件负责启动一个真实的浏览器的新实例。它建立的通信通道可以依赖于控制台的消息或通过更高级的 window.postMessage 函数调用。
我们可以使用简单的 Node.js 服务器和一个小型模板库eta来创建一个执行测试的浏览器页面。我们可以使用模板库创建一个有效的 HTML 文档,该文档包含依赖项和测试文件。为了正确加载框架文件,页面依赖于importmap(developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap)将 ES 模块名称映射到 URL。以下代码片段展示了代码:
<script type="importmap">
{
"imports": {
"componium-test": "/runner/tester.js",
"chai": "/chai.js",
"sinon": "/sinon.js"
}
}
</script>
前端测试运行器的初始实现具有最小的接口,但它可以进一步开发以显示更交互式的测试仪表板。这可能是一个潜在的功能投资,如果测试运行器在浏览器相关测试中被大量使用,可以添加额外的前端功能。导入映射结构可以在chapter6/componium-test/packages/browser/views/layout.eta文件中找到。除了框架文件外,此文件还包括解析需要包含在前端中的动态测试套件列表。这使得在前端测试工具中包含和运行多个测试套件成为可能。
测试框架
在我们开发测试框架时,我们必须确保其可靠性和正确性。然而,使用另一个外部测试框架可能并不理想,而且我们不能使用该框架本身来测试其功能,因为它仍在开发中。我们使用简单的方法来应对这一挑战,使单元、快照和端到端(E2E)测试成为可能,以确保我们的项目正确运行。
我们的项目单元测试主要可以依赖于现有的测试运行器实现。然而,我们需要集成快照测试,这包括将测试运行器的输出与期望输出的快照版本进行比较。在我们重构某些功能时,我们应该使用快照测试来验证,以检测意外的变化。包含此类测试在我们推送新的更新和改进到项目中时,能让我们更有信心。特别是,需要更多关注的单元测试与测试运行器的退出状态码相关。
另一个重要的测试补充是包含覆盖整个框架工作流程的端到端测试。这包括安装包、导入测试包和执行测试。这些测试是通过创建一个单独的测试包来实现的,该测试包将我们的框架作为依赖项,如下所示:
"dependencies": {
"componium-test": "file:.."
}
示例测试项目在package.json文件中将componium-test作为依赖项,并指向父目录以加载文件。这些端到端测试可以在项目的e2e目录中找到。在第九章中,我们将探讨改进和验证项目质量的其他方法。
框架包
如 图 6**.5 所示是项目的初始包。它们位于 packages 目录中,类似于许多其他框架选择组织他们的项目:

图 6.5:项目的框架包
这些包根据它们的职责进行逻辑划分。以下是它们简要概述的功能:
-
assert: 包含断言库相关的配置 -
bin: 包含可执行文件,由package.json文件导出 -
browser: 与浏览器环境中的测试执行相关的文件 -
views: 模板文件,使得在前端上下文中加载测试成为可能 -
mock: 模拟库相关的功能 -
node: 与 Node.js 执行环境相关的文件 -
runner: 在执行上下文之间共享的全局测试运行器接口 -
util: 在框架中使用的各种实用函数
除了将所有自开发的包组合成一个统一的设计外,我们的项目还依赖于外部库。以下是框架中用于实现所需功能集的一些依赖项列表:
-
chai: 此库允许我们快速设置一个测试断言接口,使测试体验更加愉快。它为我们提供了github.com/chaijs/chai中有用的断言接口。 -
sinon: 此库允许我们提供一个用于创建测试间谍、存根和模拟的接口。启用此功能使我们的框架更适合测试 JavaScript 应用程序,因为它允许更全面的单元测试覆盖率。 -
debug: 一个小型实用工具,通过引入过滤后的调试日志,使开发框架变得更加容易。通过在每个框架包之后命名空间调试级别,它在执行测试时使理解框架的内部结构变得更加容易。 -
eta: 这是一个轻量级的模板引擎,帮助我们构建浏览器中的测试运行器。它生成一个包含必要的框架文件和测试套件的 HTML 文档。 -
glob: 此模块使测试目录可以进行模式匹配。它允许我们运行ct tests这样的命令,其中tests是一个目录,结果是在特定目录中找到所有测试文件。通常,glob通过提供易于使用的文件模式匹配系统,为我们节省了大量编写文件系统相关代码的时间。 -
yargs: 这被用作框架 CLI 的参数解析器。它使我们能够为测试运行器创建更好的命令行体验。 -
puppeteer: 包含此库是为了向BrowserExecutor类提供一个真实的网页浏览器测试接口。puppeteer通过控制一个 Chromium 或 Firefox 实例并在该环境中运行测试,实现了无头浏览器测试。
这些是我们将在项目中使用的依赖项的亮点。对于一些框架逻辑,我们已经构建了自己的解决方案,并组织在包中。同时,我们依赖外部库来解决特定技术挑战的复杂性。当前的架构允许我们引入新的包或扩展现有的包。定义的抽象也允许我们用其他解决方案替换外部依赖。
摘要
本章为我们提供了一个了解如何将本书早期章节中的概念和架构原则整合到构建基本 JavaScript 测试框架中的指南。这种实用方法为我们提供了构建测试框架内部结构的见解。即使我们最终没有编码一个新的测试项目,它仍然训练我们的软件肌肉,使我们具备从零开始构建某物的通用知识。我们的方法结合了库和包的组合,以实现标准和独特功能的混合。
我们涵盖了框架开发工作流程的三个部分:设置新项目、制定初步设计和完成目标头脑风暴的第一版,包括架构设计和实现。随着我们将这些技能付诸实践,目标是使你作为一个开发者对架构、开发和为他人制作成功的项目感到更加舒适。在接下来的章节中,我们将关注框架的发布和维护。我们还将继续这些实践练习,当我们逐步构建更多项目时。
第七章:创建全栈框架
在第六章中,我们学习了一个构建简单 JavaScript 测试框架的实用示例。在本章中,我们将继续这种实用方法,进一步深入框架的开发。
下一个目标是开发一个全栈框架,它将使开发者能够构建大型和小型 Web 应用程序。本章将从开发此类框架的后端部分开始,重点关注服务器端组件和集成基本开发者工具。一旦建立这些后端组件,将帮助我们支持我们在第八章中创建的框架的前端元素。在本章中开发后端功能集将帮助我们完成以下工作:
-
定义我们新全栈框架的技术架构和目标。这与第六章的练习类似,但现在我们将切换上下文,更多地关注后端服务器功能的技术挑战。
-
了解构建功能齐全的全栈工具所需的组件。我们将研究和探索我们可以构建的抽象以及框架的核心部分,这将使其在许多开发场景中可用。
-
确定将提高可用性的功能,重点关注赋予开发者权力并提高效率的功能。这些包括帮助从模板自动生成框架脚手架并提高开发生产力的工具。
技术要求
实现的框架代码位于本书的代码库中,网址为github.com/PacktPublishing/Building-Your-Own-JavaScript-Framework。资源和代码位于chapter7目录中。与第六章一样,我们将利用 Node.js v20 或更高版本进行此项目。
如果您想在本地运行或调整框架文件,请参阅章节目录中框架的README.md文件。npm scripts在开发期间使用快捷方式很有用。与其他项目一样,要开始工作于框架,您需要使用npm install安装依赖项。
该框架提供了一个可执行文件,有助于创建项目的框架轮廓、运行新创建的应用程序等。要从chapter7目录本地安装componium框架的可执行文件,可以将可执行文件链接到不同的目录中使用。为此,从已签出的存储库目录,使用npm link <path>/chapter7/componium。这将链接全局框架可执行文件到您的终端 shell,您可以在示例项目中使用它。此外,如果您对框架文件进行了任何更改,可执行文件将立即获取这些更改,因为脚本是直接链接的。查看详细的npm link指令,链接为docs.npmjs.com/cli/commands/npm-link以及关于 Windows 兼容性的以下注意事项。
关于 Windows 兼容性的注意事项
在 Windows 操作系统中使用框架可执行文件和命令(如npm link)时,有一些事项需要注意。当运行npm link chapter7\componium或其他命令时,您可能会遇到一些与可执行环境相关的问题。可能会发生一些常见错误,包括以下内容。
如果您遇到与enoent ENOENT: no such file or directory AppData\Roaming\npm相关的错误,请确保在指定路径中创建该目录。这通常是 Windows 上npm安装程序的一个副作用。
如果遇到UnauthorizedAccessException问题,这是 Windows 的标准安全措施。要修复此问题,请运行Set-ExecutionPolicy RemoteSigned并允许执行。有关更多信息,请参阅 Microsoft 的文档,链接为learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy。
您还可以参考 npm 的常见错误文章。如果您能够成功使用 PowerShell 或命令提示符运行componium --version,则表示您的环境配置正确。
全栈框架目标
在我们进入这个项目的软件架构部分之前,我们需要更深入地了解一个全栈框架将包含哪些内容,特别是在 JavaScript 环境和语言生态系统中。当开发一个全栈 JavaScript 框架时,我们可以创建一系列抽象和约定,以帮助开发者根据 JavaScript 语言及其生态系统的模块化特性,生成前端和后端组件的组合。从 JavaScript 语言的角度来看,框架可以利用最新的语法和功能改进,例如 ES6 模块和现代 Web API。在生态系统方面,我们将大量依赖已经建立的模块来启用某些功能。类似于第六章中的方法,这种策略使我们能够专注于更大的系统设计,并实现更好的功能覆盖。
全栈框架的主要目标是简化应用程序开发过程,通过提供一种结构化和标准化的方式来构建应用程序,使其更快、更高效。我们在第一章中看到了这些技术解决方案的多样性,其中像 Next.js 这样的框架在整个栈上创建了一个更加流畅和有见地的工作流程。与 Next.js 类似的项目封装了开发的所有方面,覆盖了许多用例,旨在消除在不同开发层决定不同技术的必要性,并提供一个单一的统一愿景。本章中我们样本框架项目的目标之一是关注开发统一功能集的类似示例,其中组件可以自然地结合在一起。
本章中框架的实现愿景应该提供一个统一的 API,以便使用该 API 的开发者可以轻松熟悉其结构和功能,减少与新技术采用相关联的学习曲线。不同功能之间的熟悉度和互操作性确保了开发者能够获得无缝的体验。框架 API 提供了抽象(在第二章的“关于抽象”部分中突出显示),这使得复杂操作变得简单,同时不牺牲高级用例所需的灵活性。创建易于使用的公共框架 API 接口的方法对于支持开发者的不同技能水平和稳健应用的需求至关重要。当你开发或支持本章中类似实际示例的全栈框架时,你也将不得不考虑各种功能。你会发现,在你的特定场景中,你将更多地投资于对你组织来说优先级更高的功能集。
首先查看服务器端功能集,一个全栈 JavaScript 框架必须能够提供一组基本构建块来处理标准用例,基于不同的技术挑战。这些由构建块驱动的功能可能包括请求路由、数据库集成、事件、日志和性能扩展的统一接口。在某些情况下,框架还可以提供超出开发者项目需求的功能。因此,作为开发过程的一部分,我们必须在使用内置功能和提供各种可扩展性选项之间取得平衡。我们在第三章的“插件和扩展 API”部分中看到了这种情况,其中服务器端框架包括扩展和启用附加功能的方法。除了提供具有最佳开发者体验的庞大、灵活的功能集外,应用程序还必须易于部署,并在生产环境中运行时能够进行监控。在这种情况下,追求的目标是使开发者能够轻松部署他们的应用程序,随着流量的增加而扩展这些应用程序,并监控延迟和错误率。这涉及到构建设置多个服务器实例的实用程序、定义数据库迁移等。
在定义全栈框架的预期功能时,我们需要定义前端和后端功能将如何相互交互以及我们希望从哪里开始初始开发。全栈框架这两方面的考虑将在下一节中探讨。
前端功能
在全栈框架的前端方面,重点应放在提供与后端服务良好交互的前端组件的无缝集成。这些类型的功能的例子可能包括轻松从服务器获取数据、渲染前端组件以及访问静态文件。Ember.js,连同 EmberData,提供了如何有效地与服务器端系统协作的实例。另一个例子是 Next.js,它为服务器和客户端提供了一种紧密集成的解决方案。通常,有偏见的组件可以定义框架如何与网络请求接收到的数据交互,以及数据如何绑定到前端视图和组件上。
前端功能集应该能够处理标准的前端需求。这可能包括通过提供一种简单的方法将库集成到项目中,来与流行的库一起工作。一个广泛的前端解决方案应该促进客户端路由,并能够管理依赖关系、处理表单验证、用户输入净化和 JavaScript 代码打包。应用程序状态管理是另一个重要的组件,可以帮助管理大型项目的复杂性。

图 7.1:全栈框架的焦点
最终,开发一个全面的全栈框架的过程需要探索技术的许多方面。这一举措还需要广泛的编程技能。你将需要深入前端功能的复杂性,在那里你将关注用户体验,以及后端挑战,重点关注服务器、数据库和应用逻辑。开发过程将提高你的编码能力,并促进对整个 Web 开发生态系统的更好理解。在图 7**.1中,我们分解了我们框架开发的两个重要方面。功能将提供构建前端和后端组件所需的功能,与数据库等服务进行交互,而开发者体验将使这些功能可重用且易于访问,适用于应用项目。所有这些工作的结果将是一个可供各种利益相关者使用的项目,满足开发者生产能够推动新服务和基于 Web 的软件产品的 Web 应用程序的需求。
后端功能
我们首先从后端功能开发开始。这种方法将为项目提供一个基础,以便稍后在其之上添加前端功能,以完成全栈功能集。在下一章中,我们将重点关注前端功能和架构。目前,我们将假设我们的后端框架可以向浏览器客户端发送文件,并且可以与任何前端框架一起使用。基于浏览器的应用程序可以向服务器端组件发出请求,以获取数据、静态文件或网页。

图 7.2:我们的全栈框架标志
我们将重用第六章中建立的相同 Componium 项目标识和品牌。我们现在有一个新的标志,如图图 7**.2所示,作为品牌示例,以识别我们的全栈项目。Componium 术语和品牌将涵盖框架的所有部分,并将被用于命令行界面。考虑到目标开发者用户群,以下是为该项目列出的一些潜在的后端入门功能:
-
API 部署:部署 API 服务的能力。API 功能应该足够灵活,使我们能够轻松配置端点并添加功能中间件到所选端点。API 功能还应包括定义 GraphQL 资源和模式时的模块化方法。
-
页面渲染功能:提供带有自定义和可重用布局的网页,其中包含静态和动态数据的能力。
-
中间件机制:可以添加各种类型的服务器中间件(如身份验证)到应用程序的所有或某些路由中。
-
数据库功能:提供使用不同类型数据库技术的数据库对象关系映射(ORM)功能。
-
应用程序可伸缩性:设计用于处理高负载情况并提供可伸缩性和性能优化选项。该框架应提供必要的工具来测试、调试和部署应用程序。
-
开发工具:该框架提供各种辅助工具以简化开发过程。这些工具可能包括应用程序引导和标准组件的脚手架搭建。通过避免每次更改手动重启进程,实时重新加载后端服务器也可以节省大量时间。
这些功能目标现在定义得足够详细,涵盖了前端和后端体验。利用本节中的考虑因素,我们已准备好进入构建全栈框架初始部分的激动人心的步骤。
架构
现在我们对后端功能有了更清晰的定义,我们可以开始设计框架的包和组件。我们需要对服务器和路由接口进行封装,以及额外的组件,使我们能够与数据库通信,获取特定应用程序配置,并通过日志记录报告指标或日志。

图 7.3:请求生命周期
图 7.3 表示了我们框架将处理的请求生命周期的简化视图。请求可以来自浏览器客户端,或者可能是直接请求到我们框架需要处理的服务的 API 端点。您可以在章节目录下的 tests/sample 中找到使用我们框架构建的示例应用程序。

图 7.4:一个示例应用程序布局
我们现在对框架如何通过其内部处理请求有一个概述。图 7.4 展示了一个应用程序布局的示例,包括后端 API 和 GraphQL 路由。开发者可以利用这种结构来构建不同类型的后端应用程序。在接下来的章节中,我们将深入探讨使这种应用程序布局成为可能的入口点、路由器、配置、数据库和可观察性功能。您可以在章节目录下的 componium/tests/sample 中找到文件。其中一些文件,如 *.log 文件,将在应用程序启动并产生一些日志时自动生成;通常,这些日志文件会被版本控制系统忽略。
入口点
服务器文件是我们后端框架提供大部分功能的入口点。它启动一个进程,加载适当的配置,如果指定则连接到数据库,并建立请求路由处理程序。在第六章中,我们有一个更简单的服务器(位于项目文件componium-test/blob/main/packages/browser/server.js),使用http.createServer(...) Node.js API 开发。我们可以遵循相同的策略,从头开始构建一个新的服务器。然而,对于一个全栈框架,我们需要一个更成熟的解决方案,它已经提供了许多服务器接口。对于这个项目,一个很好的折中方案是使用express模块并在其之上创建抽象。Express将允许我们定义中间件、自定义路由器、解析请求、构建 API 响应等。如果这听起来很熟悉,这是因为这与第四章中预览的 NestJS 框架项目架构相似。
为了扩展服务器功能,我们还将允许开发者在同一应用程序上下文中创建多个服务器,如下所示:
import Componium from "componium";
const app = await Componium.initialize();
const server = await app.createServer({...});
在服务器接口就绪后,我们现在可以在端口上接受请求,并使用符合我们要求的选项配置服务器。一旦创建服务器,如前述代码所示,其主要目的是处理端点并执行后端任务。为了处理端点,我们将引入路由接口。它在接受请求并启用 API 功能时发挥作用。
路由器
在这个框架中,我们希望提供基于文件和动态路由的选项。这涉及到使用与 Next.js 中基于文件的路由器类似的功能。我们在第二章的构建块概述中看到了这个概念。路由器的实现可以在框架的服务器包中找到 – 例如,基于文件的路由器在packages/server/file-router.js。
基于文件的路由实现能够从目录结构中递归地加载 API 端点。基于文件的功能允许进行一些手动配置。默认情况下,框架可以在项目的routes目录中查找。然而,这可以通过向服务器配置传递文件路由目录选项来调整。有关基于文件的路由实现详情,请参考以下包 – packages/server/file-router.js。
由于我们需要在文件路由中处理不同的 HTTP 方法,我们可以为 HTTP 方法建立一个模式。这样,如果你发起一个POST或DELETE请求,你仍然能够表明文件应该响应哪种类型的 HTTP 方法。为了启用此功能,我们将配置框架以以下方式处理文件。以.post.js结尾的文件将表示此路由是一个POST方法路由。省略此配置的文件将默认为GET处理器。为了处理索引或根端点路由,我们将使用一个_index.js文件。这表示它是根处理器。例如,api/目录可以有一个_index.js文件,它将处理对 API 根端点的请求。请参阅图 7**.4以查看api/目录的目录结构。你可以在 GitHub 仓库的示例应用程序中找到这个例子,网址为componium/tests/sample/routes/api/_index.js。
除了基于文件的路由之外,开发者有时需要在他们的代码中直接或通过编程方式定义路由和模块化路由器。为了支持这些用例,我们的框架提供了几个服务器方法,以允许在路由、中间件和自定义路由器周围具有灵活性,如下面的代码块所示:
server.addMiddleware("middleware1", () => {
console.log("Server middleware log");
});
server.addRoute("/dynamic", function (request, response) {
return response.send("Dynamic Path");
});
const router = server.createRouter("/router");
router.addMiddleware("routerMiddleware", (request) => {
console.log("Router middleware:", request.originalUrl);
});
router.addRoute("/sample", function (request, response) {
return response.send("Router Sample");
});
阅读前面的中间件代码,我们可以看到我们的框架提供的某些方法,以启用动态路由。在express包的原始基础上,我们可以创建一个更抽象的系统,它提供了一种自定义的方式来描述路由定义。一如既往,它也允许我们在需要时替换express模块。addRoute方法存在于服务器和自定义路由器对象上。这些允许我们添加自定义路由处理器,而不是使用基于文件的处理器。addRoute函数接受一个路由路径和一个具有请求/响应参数的函数作为参数,用于处理请求。这个路由处理流程在上面的代码中得到了可视化,它创建了一个服务器中间件、一个新的路由以及一个具有自己的路由实体的额外路由器。有关路由器功能的更多详细信息,请参阅expressjs.com/en/guide/routing.html。
除了路由方法之外,我们还有额外的中间件管理函数,例如addMiddleware。这些方法允许我们配置任何自开发的或外部中间件。在这种情况下,它使我们的框架与基于 express 的中间件兼容。例如,我们可以使用它通过passport模块(github.com/jaredhanson/passport)对我们的端点添加身份验证;这将使框架能够支持大量的身份验证选项。
路由器和入口点的既定架构使我们能够实际创建一个可以接受请求的应用程序进程。在以下小节中,我们将定义配置我们框架不同组件的能力。
配置
开发者期望有一种干净的方式来配置基于框架的应用程序,并具有在本地、测试和生产环境中运行它的能力。为了实现这一点,我们将利用config包(位于npmjs.com/package/config)。这个包将允许我们优雅地组织配置选项,并让应用程序代码库根据环境表现出不同的行为。
以一个简单的例子来说明,如果我们只有一个属性需要配置,那么我们的配置文件default.json可能看起来像这样:
{
"database": {
"connection_uri": "sqlite::memory"
}
}
使用config包,我们可以在框架的任何模块中访问配置,并允许它在代码库内用于任何开发目的:
import config from "config";
console.log("config", config.get
("database.connection_uri"));
config 包自动加载所需的文件,并允许我们从配置存储中获取数据,访问所有设置的属性。在先前的配置代码中,我们可以获取用于启动数据库连接的数据库连接 URL。
与其他模块一样,我们可以在config包上创建抽象。然而,我们希望为开发者提供包的核心功能。config包支持多种应用程序配置文件格式,如 YAML (yaml.org)、JSON 或纯 JavaScript。提供格式选择是有帮助的,因为它使我们能够满足应用程序部署工具的要求。例如,开发者运营可能更喜欢使用 YAML 文件来结构化所有生产配置。
在框架内配置应用程序的各个方面可能会变得非常复杂,因为可用的配置格式和方法种类繁多。JavaScript 生态系统提供了数十个可以帮助你进行配置的包。尝试评估其中的一些,以找到最适合你需求的那个。考虑一个包支持的配置文件扩展类型以及它可以执行什么样的验证。此外,在以下小节中,我们将强调你的框架应该如何提供一组适用于用户的默认值。
合理的默认值
config 包的功能将允许我们在default.js文件中指定默认值。此文件将作为应用程序配置的模板。配置文件也可以以不同的方式构建;我们将在开发者 体验部分进一步探讨这一点。
减少框架中的配置量是值得追求的。这是创建一致体验和减轻使用框架构建新项目负担的关键。
数据库
随着我们向框架添加database功能,我们需要确保支持多种数据库。项目还应提供一种统一、抽象的方式来处理数据库对象。为了支持这些用例,我们可以依赖一个支持 ORM 的更高抽象库。在本章的示例中,我们将依赖一个名为Sequelize的开源 ORM 库(sequelize.org)。这个库为我们提供了 MySQL、PostgreSQL 和 MSSQL 的原生支持。它还使我们能够在本地开发和其它环境中使用 SQLite,这使得数据库操作变得非常容易,而不需要连接到更复杂的数据库服务。像 Sequelize 这样的 ORM 库将添加使用 JavaScript 与数据库交互的能力。它将允许我们将数据库实体视为对象,简化数据操作和提取。
在 Componium 中,数据库接口可以在db包中找到。虽然与 Sequelize 交互的一些部分被抽象化,但我们仍然依赖框架的用户直接熟悉这个库及其功能。由于实现和支持多个数据库引擎需要大量的代码和时间投资,因此看到大型框架依赖于单独的 ORM 层是很常见的。
当在代码中初始化新应用程序时,框架会自动尝试连接到配置的数据库。为了与数据库实体操作,应用程序需要加载模型文件。在 Componium 框架中,模型存储在models目录中,并在初始化时加载。这种应用程序结构允许框架用户保持数据模型有组织和模块化。然后,您可以在路由处理程序中使用这些模型与数据库交互,无论是创建、读取、更新还是删除数据。以下是我们可以在应用程序中使用的模型示例:
import { DataTypes, Model } from "sequelize";
class Package extends Model {}
Package.init(
{
title: DataTypes.STRING,
address: DataTypes.STRING,
created: DataTypes.DATE,
},
{ sequelize: componium.db, modelName: "package" }
);
要在我们的路由处理程序中访问或修改此模型,我们可以使用componium对象来获取数据库实体对象并执行操作:
export default async (req, res) => {
const packages = await componium.models
["package"].findAll();
componium.logger.info(`Found packages ${JSON.stringify
(packages)}`);
res.json(packages);
};
之前代码位于应用程序的sample/api/packages.js中,它查询存储在Package模型中的包。最后,它在路由处理程序中返回查询中找到的所有对象。在本章中,数据库实现相当简单,但如果愿意,你可以接受挑战,使代码更适应处理多个数据库并提高模型文件耦合度。
GraphQL 支持
除了定义 API 端点的几种选项之外,我们的框架还支持GraphQL,这是一种强大的查询语言,对于后端数据检索非常有用。一些开发者可能更喜欢在我们的框架中使用 GraphQL,并且他们应该有良好的集成此系统的经验。
GraphQL 模块可以在packages/server/graphql.js的章节文件中找到。遵循类似文件路由的设计,componium框架有一个特性,使其更容易为 GraphQL 开发模块化模式。这些模式可以在单独的文件中定义,稍后组装成由每个 Componium 服务器对象支持的完整模式。每个 GraphQL 类型定义都可以在其自己的*.gql.js文件中定义。以下是一个packages.gql.js文件的示例:
const typeDefs = `#graphql
scalar Date
type Package {
title: String,
address: String,
created: Date,
}
type Query {
packages: [Package]
}
`;
首先,我们为包类型定义一个示例定义,以便我们可以查询该特定模型的数据:
const resolvers = {
Query: {
packages: async () => {
const packages = await componium.models
["package"].findAll();
componium.logger.info(`Found packages ${JSON.
stringify(packages)}`);
return packages;
},
},
};
export {typeDefs, resolvers };
上述代码使用了包类型的模式定义,该定义查询与Package模型相关的对象的条目,检索数据库中存储的所有包。.findAll()调用是底层的 ORM 包内置的接口。

图 7.5:Apollo GraphQL 沙盒请求
现在可以使用查询语言检索数据库项。图 7.5显示了@apollo/server包的界面。通过在创建新的 Componium 服务器时指定gql选项,可以通过调用app.createServer({gql: false})方法来启用或禁用 GraphQL 支持。
如果你想要了解更多关于 GraphQL 的信息,可以查看packtpub.com/search?query=graphql&products=Book上与之相关的出版物,特别是《使用 GraphQL 和 React 进行全栈 Web 开发》这本书。GraphQL 的许多方面可能会迅速变得非常复杂,但它是一个很好的例子,说明你可以将这个特性添加到你的框架中,以实现基于既定规范的强大查询语言。
下一个子节重点介绍可观察性,这将有助于确保 GraphQL 特性在应用程序中正常工作,并且我们可以通过日志记录获得足够的项目内部可见性。
可观察性
值得特别强调的最后一个特性是可观察性。我们的框架应该提供一个接口,允许开发者将关键操作记录到文件中。框架本身应该自动记录开发者应该注意的任何敏感操作。
这就是基于winston的Logger类发挥作用的地方;你可以在框架的packages/app/logger.js文件中找到其实现。winston (github.com/winstonjs/winston)是一个可以以灵活方式输出日志的日志库,并且与各种部署环境兼容。它提供了一个简单直观的日志记录接口,允许开发者以最小的努力将日志记录轻松集成到他们的应用程序中。这不仅确保了开发者可以对记录的内容有细粒度的控制,而且也标准化了整个应用程序中日志的生成方式。
日志记录器类旨在对日志环境具有灵活性。它被配置为在非生产环境中将日志记录到控制台,允许在开发阶段进行实时调试。在生产中,日志将被写入指定的文件,提供应用程序操作和行为的永久记录,在需要时可以参考。这种环境分离还提供了一定程度的安全性,确保在开发过程中不会意外泄露敏感数据。这些日志可以用来诊断和排除问题,增强应用程序的可观察性和可靠性。如之前在 GraphQL 支持 部分讨论的,我们将使用日志记录器来跟踪找到的包:
componium.logger.info(`Found packages ${JSON.
sstringify(packages)}`);
在开发模式下,这个日志记录器将在终端控制台中提供信息;这样,在功能开发阶段可以轻松地看到这些信息。一旦应用程序部署并运行在生产环境中,它将记录到 error.log 和 combined.log 文件中。这些文件中的日志格式将是 JSON,如下所示:
{"level":"info","message":"Found packages [{\"id\":1,\"title\":\"Paper Delivery\",\"address\":\"123 Main St.\",\"createdAt\":\"2023-02-01T05:00:00.000Z\",\"updatedAt\":\"2023-02-01T05:00:00.000Z\"}]","service":"package-service"}
这个 logger 类提供了记录 .info、.warn 和 .error 消息的接口。它将通过提供一种跟踪应用程序行为、调试问题和存储必要日志的方法来赋予开发者权力,这些日志可以在以后被解析。
在总结 架构 部分时,我们现在对入口点、路由、日志记录、数据库和端点支持有了深入了解。这些都是项目第一迭代中的大量功能。进一步来说,我们将把这些功能与改进的开发者体验相结合。
开发者体验
在构建了几个基本的后端功能之后,我们现在可以专注于开发者体验方面。如之前在 图 7**.1 中提到的,这是框架我们将关注的第二个重要方面。引入框架相关开发者工具背后的目标是减少适应我们框架特性的摩擦。它还有助于简化体验,并创建一种统一的方式来与框架的原始功能协同工作。
为了启用大部分这种体验,我们将在框架安装时依赖随框架一起提供的 componium 可执行文件。这个可执行文件将负责许多日常任务,例如初始化应用程序和构建标准组件。它还将通过启用如实时服务器重新加载等特性来消除常见的摩擦点。
在接下来的三个小节中,我们将探讨三种潜在的面向开发者的体验提供方案,您可以将这些方案提供给框架的用户。
引导新的应用程序
Componium 框架中附带的可执行文件componium简化了启动新应用程序的过程。开发者无需从头开始设置新应用程序,只需运行npx componium init命令即可。此命令将提供选项以创建具有所有必要配置和依赖项的基本项目结构。初始化过程可以节省大量时间并确保使用该框架构建的不同项目之间的一致性。该命令具有接受参数的灵活性,例如npx componium init my-app,在my-app目录中创建新的应用程序结构。对于开发者来说,init功能非常出色,因为它自动化了从无到有创建新应用程序的重复和易出错的流程。
使此启动过程变得简单对于新开发者开始使用该框架至关重要。回顾第一章,你会发现几乎每个框架都提供一种或几种启动项目的方法,使用 CLI 工具或类似机制。
搭建
为了继续探讨减少易出错任务的主题,搭建是一个高度有益的功能,它进一步增强了开发者体验。它允许开发者自动为框架支持的组件生成样板代码。在本章的框架练习中,我们将支持创建诸如模型、GraphQL 模式、路由等事物。

图 7.6:搭建体验示例
图 7.6展示了运行框架可执行文件以在新应用程序内部搭建一些新组件时的开发者体验。可以为每个选项提供高度详细说明,以帮助解释每个操作的潜在选项。create命令还可以支持直接设置参数,以避免框架高级用户使用Select界面。搭建命令可以展示最佳实践并快速教会开发者关于框架功能的知识。
此外,新的项目初始化搭建选项可以包括开发者想要使用的框架功能。例如,他们可以选择默认设置或自定义项目,并选择他们的数据库类型,启用 GraphQL 支持,选择基于文件的路由器等。要成功执行此工作流程步骤,框架必须提供有关初始化和设置项目的清晰说明。
搭建在许多项目中都非常常见,对于新项目来说是必备的。在某些情况下,搭建功能可以与主项目分开提供。然而,提供搭建能力可以极大地提高与框架的交互。
文件监控
文件监视是componium可执行文件提供的另一个有价值的功能。在这种情况下,它通过componium dev命令用于内部使用。此功能监视应用程序源代码文件中的更改。一旦检测到更改,它将自动重新构建并重新启动服务器。这意味着开发者可以更快地看到他们更改的结果,并更快地进行迭代。随着我们进一步开发框架的后端功能,这种文件监视基础设施将非常有用。它也可以对下一章有益,在那里我们将介绍需要随着迭代而重新构建的前端组件。
这些功能的组合——易于初始化、详细的代码脚手架和文件监视——为开发者创造了一个出色的体验,因为它允许开发者专注于项目需求。我们将在下一节中看到这些开发者功能的相关性,因为它们与利用我们新开发的框架的示例工作流程相关。
三个改进开发者体验的示例有助于定义框架作者可以投资的特定任务。这些增强旨在提高项目的整体可用性。在下一节中,我们将讨论如何利用这些脚手架和文件监视工具,以及它们如何伴随框架项目,使其使用更加愉快。
开发者工作流程
在本章的前几节中,我们确定了项目目标,开发了架构,并描述了框架的一些示例功能。在本节工作流程中,让我们总结一下开发者如何利用项目并了解他们的工作流程。
下面是工作流程的各个部分,包括框架开发者侧需要执行的一些步骤,以使项目可供使用。
框架分发
新创建的 JavaScript 框架在npm包注册库中公开发布或私下发布。当框架作者希望将项目完全保持私密时,它可以从内部基础设施中的私有 Git 仓库中消费。整个框架包包括一个README文件和框架文档,这些文档有助于外部开发者开始构建新的应用程序。
新项目需求
以框架使用的一个例子来说,开发者想要构建一个跟踪包的示例应用程序。系统需要能够列出进站和出站的包,更改这些包的属性,并添加和删除新的包。最终,开发者希望启用这些功能并将它们部署到远程环境中,在那里应用程序与生产/预发布数据库环境交互。
开始一个项目
要开始构建应用程序,开发者需要安装框架。他们可以使用如npm install -g componium这样的命令。该命令全局安装框架。这类似于我们之前使用的npm link命令,用于获取对可执行文件的访问权限。然而,在这种情况下,框架是从 npm 包管理器数据库中下载的。在你的情况下,如果你觉得这样更容易,可以使用可执行的 npm 链接版本。
安装框架后,开发者现在可以运行componium init来创建一个新的应用程序结构。终端将显示以下内容:
> cd new-app
> componium init
Creating /Users/user/dev/new-app/app.js...
...
Installing dependencies...
New Componium application initialized.
使用框架
初始化脚本会自动安装项目所需的部分,包括一个package.json文件。根据这些选择,代码将必要的文件构建到项目目录中。现在,起点为开发者留下一个全新的项目目录,他们可以在其中开始开发。这个工作流程的预期确保了安装过程中的最小摩擦,并向开发者展示了运行应用程序代码的示例。开发者的下一步是使用componium dev或componium create命令。第一个命令dev将在项目目录中启动开发服务器,第二个命令create可以在项目内部构建新的组件。开发者需要决定他们是否想使用脚手架助手或根据提供的框架文档从头编写代码。这两个命令将在工作流程的下一部分中派上用场,其中开发者可以开始添加新的 API 模型和服务器端点。
创建 API 和模型
现在,通过遵循关于路由创建和处理的文档,开发者可以创建必要的端点来支持他们正在构建的项目需求。例如,他们可以从使用基于文件的路由器来定义两个端点开始——一个用于创建新的包条目,另一个用于列出它们。为此,他们可以创建一个名为api的目录并添加一个新文件,api/packages.js。你可以在chapter7/componium/tests/sample/models/package.js找到这个文件的示例。
而不是手动进行,脚手架工具还可以帮助生成一个新的路由文件和一个新的模型文件,然后将它们放置到正确的目录中。对于路由生成,命令看起来是这样的:
> componium create
? What would you like to scaffold? Route
? Enter a name for your ROUTE packages
这将现在提供一个端点来处理请求。开发者可能会开始寻找方法来丰富 API 端点以包含实际数据。API 创建步骤要求应用程序作者了解可用的选项和创建新端点的机制。他们很可能会从框架文档和提供的示例应用程序中学习这些内容。
扩展 API
在 API 路由功能正常后,现在是时候启用数据持久性了。在这种情况下,开发者需要添加数据库功能来保存和列出包。框架已经确保它可以连接到本地的开发数据库,因此这一步已经处理好了。工作流程的下一步是添加一个Package模型并在 API 端点中加载它。脚手架工具可以使用 CLI 提示或命令在正确的位置生成模型文件。例如,CLI 可以运行componium create来提示开发者输入数据库模型详情。为了成功完成这个工作流程任务,应用程序作者需要了解脚手架工具或框架内管理数据库模型的手动方法。一旦我们创建了模型,我们就可以更新models/package.js文件来存储不同包的属性:
import Package from "../../models/package.js";
export default async (req, res) => {
const model = await Package();
const sample = await model.create({
title: "Paper Delivery",
address: "123 Main St.",
created: new Date(2023, 1, 1),
});
const packages = await model.findAll();
componium.logger.info(`Found packages ${JSON.
stringify(packages)}`);
res.json(packages);
};
在前面的代码中,我们既可以创建新的包,然后响应数据库中所有包的信息。此外,我们可以将创建逻辑和查询逻辑拆分到不同的路由中。完整的包文件可以在tests/sample/routes/api/packages.js中找到。
到这个工作流程步骤结束时,端点现在应该有了与新建的 Package 模型交互的代码逻辑,当访问packages.js路由时可以列出记录。
添加测试
在我们的工作流程的这个阶段,我们已经有一个可以与数据库交互的工作 API。开发者已经手动使用示例请求测试了端点。他们还可以添加一些功能测试用例以确保 API 正常工作。为了成功添加测试,需要有关建议测试端点方式的文档。这些建议可以包括使用第三方测试库或来自第六章的内置componium-test库。
配置环境
在测试了路由后,现在是时候尝试将应用程序部署到远程环境,看看它是否工作正常。我们在第一章中看到的框架以部署的简便性而自豪。因此,期望尽可能使应用程序易于部署。
要成功完成这一步骤,我们的框架提供了一个生产配置文件,config/production.json。这个 JSON 文件包含了应用程序在生产环境中运行时使用的各种环境特定设置。开发者仍然需要正确理解如何安全地指定数据库信息和其他配置选项。框架文档可以指导应用程序作者建议使这一步骤正常工作的最佳方式。虽然框架提供了这个文件,但开发者仍然有责任理解如何安全地指定所需的属性。这些细节的指定方式可以显著影响应用程序的安全性和性能,因此正确完成这一点至关重要。
部署应用程序
在生产配置正确配置后,开发者现在可以将应用程序部署到他们的服务器环境中并测试新的 API。这一步骤完成了我们的示例工作流程,如果开发者能够成功测试他们的更改并与数据库交互,那么工作流程就是成功的。
这只是一个框架工作流程的例子,它使我们能够记录从安装到运行中的应用程序的步骤。可能还有更多步骤可以添加;这很大程度上取决于我们愿意探索这个工作流程有多远。例如,使用框架的中间件 API,我们可以探索如何轻松地将认证等常见中间件添加到新的端点。我们也没有涵盖应用程序作者需要前端视图来管理和与端点交互的使用案例。
确定这些类型的工作流程可以帮助我们识别摩擦和改进框架开发者体验的机会。它还确保我们了解在进一步开发项目时,可以添加哪些类型的文档和工具改进。
在应用程序部署的这个阶段,我们得出工作流程标准进度的结论。更广泛的工作流程中的附加步骤可能涉及进行更深入的数据库操作和使用框架的 GraphQL 功能。总的来说,关注这些类型的工作流程可以帮助框架作者微调利益相关者与他们的系统交互的方式。在下一节中,我们将查看使所有这一切成为可能的外部依赖项列表。
依赖项
我们在上一节中提到的流程是由几个外部库和模块实现的。以下是本章中 Componium 框架使用的一些模块的回顾:
-
@apollo/server和@graphql-tools/schema:这两个工具的组合使我们能够提供本框架项目的 GraphQL 功能。Apollo Server 能够与 Componium 服务器集成,并提供一个易于使用的沙箱来测试 GraphQL 模式。 -
Chokidar: 这是一个文件监视库,通过监视应用程序文件的变化并执行诸如重启开发服务器等步骤,从而帮助创建更好的体验。 -
@inquirer和yargs: 这些库允许我们创建componium命令行工具。Inquirer可以创建交互式终端界面,这对于 Componium 开发命令,如componium create非常有用。Yargs帮助我们处理命令行命令、标志和选项,使得为我们的项目快速开发一个简洁的开发界面变得更容易。 -
express和body-parser: 这些是底层服务器库,使得初始化 Componium 服务器和添加路由和中间件成为可能。 -
Winston: 这是底层Logging类中使用的日志库。它帮助我们为 Componium 应用程序提供向不同类型的日志记录的方式。 -
sequelize: 这是一个 ORM 层库,它帮助应用程序与各种数据库集成。 -
componium-test: 这是来自第六章的测试库,我们可以利用它来测试后端框架。 -
debug: 这是一个用于在开发期间追踪和调试框架内部问题的日志模块。正如第六章中提到的,它支持通过使用DEBUG=componium:*环境变量将调试级别限定到特定的组件。 -
config: 这是一个配置管理模块,它帮助以不同格式存储和组织应用程序配置。
其中一些模块在服务器端框架和其他 Node.js 工具中相当常见。对于您自己的框架,您可以选择我们刚才讨论的包,或者找到更适合您用例的替代方案。幸运的是,Node.js 生态系统在 ORM、日志和测试解决方案方面提供了很多选择。
摘要
在本章中,我们进一步发展了早期对测试框架的经验,通过构建一个全新的服务器端框架,该框架能够路由请求、处理 API 调用等更多功能。这支持我们的计划开发一个全栈框架,该框架涵盖前端和后端功能,组件在相同的统一愿景中相互交互。我们的目标是创建一个可用于多种应用程序用例和功能集组合的东西。
我们首先定义了我们的项目目标,然后我们后来开发了框架的核心架构方面。这个架构包括生成诸如服务器进程管理、环境配置和数据库交互等功能。为了提高可用性和提高开发者生产力,我们还专注于生成一些专注于开发者体验的功能。
这是我们框架经验中的第二次实际练习,并且希望这能让你对自己的技能开发框架更有信心。为了更进一步,下一章将专注于我们的最终挑战——为我们的全栈框架构建前端组件。下一章中前端组件的引入将使我们的新创建框架的全栈体验得以实现。
第八章:架构前端框架
在本章中,我们现在将重点转向我们在第七章中开始构建的全栈框架的前端组件。这是为我们的Componium框架示例添加新功能和构建技术设计的最终部分。前端特性是最复杂的,因为它们需要大量的浏览器领域知识、深入的 JavaScript 知识、处理复杂边缘情况的能力等等。我们将涵盖一系列关注于启用全栈框架开发环境的前端主题。以下是我们将涵盖的一些主题:
-
前端特性: 我们将确定我们框架的前端组件的功能和目标。此外,这个新的前端基础设施需要与全栈框架的现有组件进行交互,例如后端 API 路由和测试接口。
-
建筑设计: 在深入了解现有框架的低级接口后,我们将创建一个框架设计,它能够提供与自身低级接口相似的功能。这包括开发一个组件、视图和路由架构,以向网页浏览器提供内容。
-
前端模式: 了解常见的 frontend 模式和优化将帮助我们更习惯于使用现有的框架,并在未来构建新的框架。
需要牢记的是,我们只能触及其他框架提供的功能集的表面。例如,我们可以包含一个基于组件架构的客户端路由器,包括那些组件中的响应性。像 Vue.js、Angular 和 Svelte 这样的框架,以及像 React 这样的库,需要多年的开发来覆盖所有边缘情况并显著扩展功能集。在本章中,我们将专注于保持事物更接近基础,并从头开始构建几个技术部分。这应该能让你更好地理解具有前端特性的其他全栈框架的底层组件,例如 Next.js。例如,我们将使用一些直接构建在现代网页浏览器中的 Web 组件 API,以在我们的框架中实现丰富的功能集。
在本章的末尾,我们还将检查预期的流程,以了解开发者为了利用框架接口实现特定的应用开发目标而采取的一系列步骤,使用我们新构建的特性。
技术要求
本章的技术要求与第七章的技术要求非常相似。本章重用了我们在第七章中看到的框架文件,并添加了前端组件和界面。tests目录中的示例应用程序也进行了更改,以展示一些前端功能。在github.com/PacktPublishing/Building-Your-Own-JavaScript-Framework找到本书的仓库,并继续使用chapter7目录中的代码。
在目录中遵循README.md的说明以获取可用的脚本。示例应用程序可以在框架的tests/sample目录中找到。当你启动该应用程序时,它将在端口9000上可用。你可以使用http://localhost:9000/ URL 通过浏览器打开它。当你开始探索章节代码库时,建议使用调试工具来追踪元素是如何组合在一起的。请参阅第二章中的调试部分以配置可调试的环境。
定义前端框架
我们将继续从上一章的内容开始,通过重用示例Componium框架项目。在第七章中,我们创建了一些功能,使我们能够与服务器端路由交互、定义 API 和查询数据库。目前,还没有方法来开发前端组件以消费这些 API 或使用我们的框架添加视觉界面。如果没有框架的前端部分,需要构建交互式界面并使用 Componium 服务器托管的开发者将需要包含外部库,并从服务器静态地提供额外的应用程序文件。
因此,我们将通过创建几个前端功能来弥补前端功能的不足,这些功能将允许框架用户创建客户端界面。这些前端功能将模仿现有成熟前端框架的一些复杂功能。例如,我们在组件内部实现响应式功能的方法将包括基础部分,利用浏览器内置的 API。
为了开始这个过程,我们将确定我们想要支持的功能的目标。之后,我们将跟随一个示例架构设计,使这些功能成为现实,并使我们的框架真正成为全栈。
目标
前端框架设定了三个目标,这些目标将由我们创建的功能来支持。总的来说,在框架项目进展过程中,以一般的方式定义这些目标是一个好主意:
-
基于 Web 的界面:首要目标是赋予开发者创建基于 Web 的界面的能力,同时保持与框架的后端/服务器组件的连贯性。借助框架的前端能力,开发者将能够编写、托管和部署交互式客户端界面。这些界面将通过提供前端框架 API 来创建组件并将这些组件附加到基于客户端的视图中来实现。这些基于客户端的功能也应该可测试和可调试,无论是通过内置的 Componium 测试接口还是外部测试工具。
-
启用交互性:一套全面的 API,包括服务器端和前端,将有助于实现许多框架支持的项目所需的交互性。交互性功能需要使开发者能够使用熟悉的技术,如 HTML、CSS 和前端 JavaScript 来构建组件。框架还应具备包含潜在外部库的能力。例如,如果有人想在他们的 Componium 前端应用程序中创建可视化,那么他们应该能够轻松地包含外部库,如 Three.js 和 D3.js。
-
促进可重用性:我们希望包括一套框架功能,以构建复杂的应用程序。这些可能包括具有许多前端区分视图的应用程序,其中包含大量嵌套组件。这些还可以包括针对生产环境的优化集和能够管理大型应用程序代码库的能力。主要的是,我们也希望促进代码的可重用性,并指导开发者在构建他们的应用程序时做出明智的决策。一个易于扩展的功能集可以覆盖许多潜在的使用案例。如果我们架构正确,它将允许高度的可定制性。
从已经存在的框架,如 Next.js 中学习,我们也想确保包括一些更现代的功能,并在这些建议功能旁边提供令人愉悦的开发者体验。这可以包括代码生成功能,类似于我们在第七章中看到的功能。为了使框架与一些其他解决方案区分开来,我们还将设定一个目标,使用一些新推出的 Web API。利用我们在项目从头开始的优势,我们有机会评估浏览器平台上的最新发展,选择新可用的 API。作为我们的学习目标的一部分,我们还将尝试对比我们新开发的框架与成熟的框架,如 Vue.js 和 Next.js 之间的差异。
在这些目标的基础上,让我们深入了解可以支持这些目标的功能集。
功能
上一节中定义的前端目标将帮助我们指导特征开发背后的思维过程。为了支持既定的目标,让我们规划一些开发人员会发现有用并期望从框架中获得的某些技术特性。在本章中,我们将详细讨论以下一些重要特性:
-
服务 HTML 文件和内容:为了支持交互式界面的功能,我们需要添加将生成的 HTML 输出服务到浏览器请求的能力。我们需要确保我们有服务静态内容的能力,以支持额外的 JavaScript 代码、图像、媒体和其他类型文件。这一特性对于在 Web 浏览器客户端渲染内容至关重要。
-
结构化应用程序代码:我们需要赋予定义可重用交互式 JavaScript 组件的能力,这些组件具有 CSS/HTML 模板和样式功能。我们将使用基于组件的架构方法来实现这一点。组件架构特性本身将启用用户界面的开发。它可以帮助产生的结构化应用程序代码将包括独立且可重用的代码块,这些代码块作为整个 Web 应用程序的构建块。这将支持我们的重用性和提供良好的应用程序原语的目标,因为它利用了组件范式,如响应性、可组合性和模块化。这一特性的方法旨在让每个组件控制自己的状态,并根据状态渲染界面。
支持维护目标,基于组件的方法确保应用程序代码的隔离测试和可调试性。在这里,我们借鉴了其他框架的一些先前设计决策。例如,在 Vue.js 中,组件是用 JavaScript 逻辑、HTML 模板以及使用 CSS 进行样式的功能来构建的。
-
可组合性:在前面一点的基础上,重要的是要强调应用程序代码的可组合性是我们想要创建的框架的一个重要特性。基于网络浏览器内部 DOM 结构定义的嵌套结构,组件可以组合在一起以创建复杂用户界面。请参阅第二章中的组件部分,以回顾这一特性的重要性。
-
客户端路由器:我们在第二章中看到的前端框架组织核心组件之一是路由器。路由功能集对于我们的功能集至关重要,因为它负责在应用程序的复杂视图之间导航。从第七章开始,我们已经有了一个用于服务器端路由的 API,前端路由将帮助提供实现不同应用程序状态之间快速平滑过渡的功能。
-
路由器将遵循现有网络应用导航的概念,并利用相关的 Web API 修改浏览器的 URL 和历史功能。路由器的加入也将有助于创建更逻辑上组织良好的代码。
-
HttpClient辅助模块(angular.io/guide/understanding-communicating-with-http)用于与合适的后端服务进行通信。这将是我们示例项目的绝佳增强功能,特别是如果与一些特定的 Componium 服务器 定义的路线结合使用。例如,为了帮助提升开发者体验,我们可以预先生成一些已知端点的数据获取调用,并围绕这些调用创建动态接口,从而为使用我们框架的开发者节省时间。 -
服务器端渲染(SSR):我们在本书的其他框架示例中提到了 SSR 功能。我们也将这些功能包含在我们的示例框架中。SSR 方法将在服务器端渲染组件,以帮助提高应用程序的渲染性能。鉴于我们对全栈框架的开发者体验有完全的控制权,在这种情况下开发此类功能对我们来说更容易。除了性能提升外,SSR 对搜索引擎优化和页面加载时间也有益。
SSR 的内部包括在服务器端预先渲染的组件与后来由前端代码激活的组件之间的紧密协作。后端路由还应能够将组件的状态注入到预先渲染的元素中。状态可以是静态数据或从外部来源获取的信息,例如数据库。
-
生产优化:作为对开发者赋能承诺的一部分,该框架还将提供一些针对在生产环境中运行的应用程序的优化步骤。这意味着包括一些在幕后执行优化的附加内部工具,例如压缩。这类优化也更容易集成到我们的框架中,因为我们控制着服务器工具。
除了代码压缩,我们还可以探讨一些高级的 JavaScript 优化技术,例如摇树优化(tree-shaking)和代码拆分。支持静态文件处理,我们有可能优化其他媒体,如图片。一般来说,随着框架的发展,我们希望持续地进行此类优化改进,因为这会惠及所有框架用户。
上述列表是应选的一组功能,这些功能应使前端功能丰富,为我们提供良好的学习机会,并涵盖实际用例。
下面的 图 8.1 总结了所列功能之间的交互方式:

图 8.1:功能总结
现有的服务器端代码能够提供静态文件,这些文件可以被前端使用。同时,服务器进程能够导入并访问一些组件,以便在服务器端渲染它们。最后,后端定义了客户端路由;这些是浏览器可以访问并渲染在客户端的前端端点。
在前端方面,我们有与服务器或外部服务上托管的外部 API 进行交互的API 交互功能。同时,客户端路由器与组件架构紧密协作,以实现浏览器中的用户界面体验。最后,我们有一套前端优化,涵盖了所有前端表面,确保在生产环境中部署和运行应用程序时获得最佳优化体验。
考虑到这些功能,让我们继续到架构步骤,在那里我们可以探索使这些功能成为可能的技术和组织概念。
建筑学
在概述并记录了所需功能后,让我们从第七章扩展现有的架构。这些更改将向框架架构添加新功能,重点关注启用ClientView抽象和功能,这将推动前端更改背后的功能体验。
我们已经具备了创建服务器 API 端点的功能。特征的架构的一般实现将包括向框架的服务器部分引入几个新的接口。新添加的前端功能将位于框架项目的frontend目录中。

图 8.2:Componium 前端组件
在图 8.2中,我们概述了由 Componium 服务器框架驱动的服务器接收到的请求。具体来说,这个请求预期会响应一个 HTML 页面,以提供交互功能。这与第七章中的 API 请求不同,我们会收到包含数据的 JSON 或 GraphQL 响应。请求处理器仍然可以处理传入的请求对象,以便访问请求的属性,例如查询参数。它还可以调整响应对象的任何属性。在第七章中,我们使用server.addRoute(...)来添加新路由。为了添加提供功能的路由处理器,我们将使用类似结构的server.addClientView(...)方法,它将具有类似的 API,但行为完全不同。这个新方法就是图 8.2中的客户端视图功能所在之处。
在 Componium 前端组件设计中,客户端视图可以有一个单独的视图和许多在其中使用的组件。客户端视图的概念是服务器端定义的,而视图的概念则在服务器和客户端之间共享。一旦定义了客户端视图,它将组装所有导入的组件和视图,并将响应发送回浏览器。
除了客户端视图交互之外,服务器现在还可以定义和访问静态文件目录。这些静态文件可以直接由浏览器访问,整个目录都暴露给 Web 服务器。这些静态文件也可以由客户端视图使用,将额外的资源导入客户端视图,例如任何媒体文件(图像、字体、样式等)或额外的 JavaScript 组件。静态文件的易于访问简化了前端框架如何包含外部媒体和其他可以在 Web 应用程序中包含的有用实体。
在下一节中,我们将详细了解客户端视图如何成为前端功能和文件的大门,使我们能够创建多个端点,服务于 HTML、CSS 和 JavaScript 代码。
入口点
为了在创建多个前端客户端视图时提供灵活性,我们的框架提供了一种定义多个客户端端点的方法:
server.addClientView("/", "frameworks", {
title: "Frameworks",
// …
});
上述代码是路由我们的服务器根路径到frameworks视图的示例。开发者可以在views目录中创建一个frameworks.js文件,通过定义的名称映射此视图。.addClientView(...)可以配置多次,将多个视图附加到不同的路由处理器。frameworks.js的内容结构与路由处理器从第七章中的结构相似:
export default function frameworks(request, response) {
return `<p>Welcome!</p> `;
}
上述代码可以简单到只返回一个段落标签。你可以在tests/sample/views/目录中找到其他更复杂的视图示例。视图处理器可以访问路由的Request和Response对象来调整路由的行为或获取额外的数据。我们还可以访问这里的componium变量来访问框架的接口。为了简单起见,我们将在更复杂的模板中使用 JavaScript 模板字面量。框架负责渲染所需的 HTML 结构,并且它还将上述代码中的<p>标签包装在一个有效的 HTML 文档中。
我们前端文件的其他重要入口点是静态文件配置。为了能够解析其他类型的媒体,框架提供了一种方法来标记应用程序项目中的某些目录作为提供静态文件的端点:
server.addStaticDirectory("/images", "img"));
.addStaticDirectory(...) 方法将服务器路由 /images 映射到应用项目的 img 目录。这依赖于我们之前在 第七章 中使用的后端 express 服务器相似的属性。这个新的静态目录在自定义路由器中也能工作,这些路由器是通过 server.createRouter(...) 创建的。
我们现在有了处理和渲染基本视图的方法,并且可以创建任意数量的视图。我们现在需要启用基于组件的架构。这将使我们能够生成更复杂的交互式组件。
反应性
要实现我们前端框架的动态界面特性,我们需要更多地了解反应性和反应组件的概念。在应用程序编程中,反应性概念指导用户界面动态地更新和响应底层数据或状态变化。在 JavaScript 的上下文中,尤其是在前端系统中,我们结合自定义开发的原语和浏览器 API 来实现性能良好的用户界面反应性。反应性功能通过在相关底层数据变化时自动更新界面,为 Web 应用程序消费者提供无缝体验。与其他编程环境类似,在 JavaScript 世界中,开发者依赖于框架和辅助库来为他们的应用程序中的组件启用反应性。实际上,JavaScript 中对外部工具的依赖性比其他语言要强得多。这主要是因为网页和 Web 应用程序开发的跨浏览器和跨引擎特性。
由于 JavaScript 的异步特性,反应式编程范式在 Web 开发环境中变得非常合适。对反应范式有贡献的因素还包括对实时更新、复杂交互和简化应用状态管理的需求。用户界面变得更加复杂。如今,Web 应用程序的期望要求前端系统具有动态性、消耗实时数据,并对用户操作做出即时响应。此外,围绕 Web 浏览器的 文档对象模型 (DOM) 的现有结构和抽象迫使解决方案基于操作页面元素的嵌套节点树。界面中的反应式更改利用差异算法来更新组件树中已更改的节点。
反应性需要数据绑定,这是数据与应用程序元素之间的连接。在 Web 开发世界中,数据将由 JavaScript 接口提供,很可能是从某些 API 端点动态加载的。元素将是浏览器客户端中的HTML/DOM结构。当底层数据更新时,相应的浏览器元素会自动更新以反映这些变化。在第二章的框架构建块部分,我们强调了数据绑定在这些反应性组件中可能发生的方式。我们还看到了使用单向或双向绑定的框架示例。这种流程在很大程度上取决于架构决策,要么允许元素在数据更改时更新,要么也允许元素更新底层数据。流行的框架,如 Vue.js 和任何包含 React 的框架,都使用虚拟 DOM树的实现来渲染应用程序的状态,以数据变化。然而,也有一些例子,如涉及 Svelte 和 Angular 的项目,使用真实 DOM 或shadow DOM功能来实现类似的功能。
与其他前端项目类似,我们将把反应性组件的概念引入我们的框架。这些组件将允许我们封装 HTML 元素和与之相关的应用程序逻辑。前端Componium组件将保持其内部状态并对数据和交互做出响应。为了使事情更简单,并且不深入现有前端框架的内部,我们可以使用 Web 组件和其他更现代的 Web API 的组合来构建基本反应性概念的示例。组件架构的实用概述将提供良好的学习机会,以了解内置的浏览器原语。它还将提供良好的比较和理解现有框架为我们解决复杂问题的能力:
class ReactiveComponent extends HTMLElement {
constructor() {
super();
this.state = this.reactive(this.data());
this.attachShadow({ mode: "open" });
}
reactive(data) {
return new Proxy(data, {
set: (target, key, value) => {
target[key] = value;
this.update();
return true;
},
});
}
callback() { this.render(); this.update(); }
render() { this.shadowRoot.innerHTML = this.template(); }
// methods that child components will override
update() {}
data() { return {}; }
template() { return ""; }
}
上述代码是我们新定义的ReactiveComponent;它已被压缩以更好地适应本章。该类从HTMLElement扩展开始。此接口将帮助我们表示 HTML 元素并创建我们自己的 Web 组件。在constructor方法中,我们声明了state属性,该属性将跟踪组件的状态。另一个重要的构造函数调用是.attachShadow()。此调用将 Shadow DOM 附加到自定义元素,并为封装的 CSS 和 JavaScript 指令提供了一个作用域环境。
额外阅读
要详细了解 Shadow DOM 的结构,请查看 MDN 页面developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM。
在reactive方法中,我们配置一个新的Proxy对象,这是另一个内置的 Web API,具有可以帮助我们对我们组件的状态进行反应性更改的属性。Proxy方法接受一个对象并返回一个新的对象,该对象作为原始定义的代理。代理行为有助于在对象更新时触发组件的更新和重新渲染。
其他阅读材料
有关Proxy接口的详细信息,请参阅 MDN 上的详细文章:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
当状态改变时,将调用设置操作和更新方法。update()方法在继承此ReactiveComponent类的组件中被覆盖。使用ReactiveComponent,我们可以构建一组简单的示例组件。这种结构将绑定组件状态中的数据到渲染的模板中。

图 8.3:基本组件示例
图 8**.3 展示了两个组件在操作中的示例视图;一个是年份跟踪器,另一个是月份跟踪器。这两个组件都有增加相应日期值的选项。您可以在tests/basic-components/index.html文件中找到此工作示例的代码:
<body>
<h1>Basic Components</h1>
<year-component></year-component>
</body>
视图的源代码如下;它仅包括通过其名称year-component定义的新组件,并用常规 HTML 标签包裹。这是通过使用内置的 Web Components 接口注册自定义元素实现的customElements.define("year-component", YearComponent);。year-component组件扩展了前面的ReactiveComponent类,并覆盖了空的update和template方法,如下所示:
template() {
return `
<button id="addYears">Add Years</button>
<div id="yearCount">Year: 0</div>
<month-component></month-component>
`;
}
connectedCallback() {
super.connectedCallback();
this.shadowRoot.querySelector
("#addYears").addEventListener("click", () => {
this.state.yearCount++;
});
}
update() {
this.shadowRoot.querySelector(
"#yearCount"
).textContent = `Year: ${this.state.yearCount}`;
代码中列出的方法,如update()和template(),渲染与组件相关的数据并定义由year-component组件返回的模板。我们还有事件处理器,用于更改和更新年份,通过访问this.state。此外,请注意,要访问此Year组件的 Shadow DOM 属性,我们使用this.shadowRoot.querySelector。组件中定义的模板包括month-component,这是一个嵌套组件。它具有与ReactiveComponent扩展类类似的设置。
此组件配置,使用 Web Components API 和其他浏览器 API 提供的便利性,是框架的良好起点。我们可以使用这些模式在其他框架中实现类似的功能,例如 Vue.js,以及使用 React 作为其底层库来构建其组件的框架。我们拥有的接口具有反应性属性、同时组合组件的能力以及模板化的基础知识。
在下一节中,我们将进一步探讨并利用外部 Web Component 辅助库来构建这个模式。
改进组件
在架构的响应性部分,我们描述了一种使用由其他技术组成的 Web Components 来实现所需功能集的模式。为了进一步增强这一点,我们将引入 lit.dev),这将帮助我们使管理组件的工作更加直接,并且我们的框架可以依赖于它。我们将采用相同的方法进行抽象,并利用这个库的接口来创建我们框架的组件功能。
关于 Lit 库
Lit 开源库已经存在了六年多,其目标是简化并抽象掉一些与 Web Components 相关的冗长任务。在其核心,它提供了响应式状态功能、作用域 CSS 样式、模板化以及一系列高级功能来组合和操作前端组件。它支持 JavaScript 和 TypeScript 语言,并附带大量扩展其功能的包。例如,Lit 有额外的工具来启用本地化、动画化前端元素,并且还可以与 React 组件集成。
该库除了使用标准的组件生命周期外,还使用自己的组件生命周期方法来简化某些操作,例如 DOM 响应性。您可以在 github.com/lit/lit 和 lit.dev/docs 找到项目的源代码。
为了与先前的 ReactiveComponent 接口进行比较,让我们看看如果我们使用 Lit 库,代码会与基本组件有多相似:
import {html, css, LitElement} from 'lit';
export class YearComponent extends LitElement {
static styles = css`p { color: green }`;
static properties = {
year: {type: Number},
};
constructor() {
super();
this.year = 2024;
}
render() {
return html`<button id="addYears">Add Years
</button>
<div id="yearCount">Year: ${this.year}</div>`;
}
上述代码导入了我们可以扩展的 LitElement 类。这个新的代码块结果看起来与我们在 响应性 部分看到的 year-component 代码非常相似。然而,在这个组件定义中我们还有一些额外的改进。
让我们探索其中的一些:
-
首先,我们有一个 CSS 辅助接口,它允许我们使用
staticstyles变量来声明我们组件的样式。 -
其次,我们声明组件状态的方式也有所改变——我们定义了一个包含一些额外属性定义的
static properties对象。这些属性在模板中以类似的方式使用。 -
这引出了第三个要点——模板化也有所不同。它使用
lit-html辅助器来启用更高级的模板化功能并帮助我们处理 HTML。这个辅助器允许我们创建模板指令,调整渲染方法,等等。
额外阅读
所有模板化功能的详细文档可以在 lit.dev/docs/v3/templates/expressions 找到。
为了我们示例框架的目的,我们将定义自己的ComponiumComponent类。这个类将可供开发者使用,以创建丰富的组件,结合我们从响应性部分和 Lit 库中学到的知识。我们还可以依赖 Lit 模板的丰富功能来渲染结果。为了启用这一点,我们将加载 Lit 库和componium.js框架文件一起在ClientViews中。这将使组件接口暴露给开发者定义的组件。要开始使用这些接口,开发者可以使用 ES6 语法导入它们:
import { css, html, ComponiumComponent } from "componium";
提供了要扩展的类,以及 CSS 和 HTML 辅助工具,以帮助构建组件。例如,如果我们的组件有交互式按钮,它可以使用以下 Lit 语法来声明事件处理器:
html`<button @click="${this._click}">click</button>`;
_click事件处理器是在从ComponiumComponent扩展的类上定义的方法。如果这些组件需要任何静态文件,它们可以直接通过从 Componium 服务器声明的静态路由中获取它们来请求。然而,我们仍然可以更进一步,使用 Lit 和我们的框架接口的混合来启用 SSR 的复杂功能。关于从服务器利用现有组件的概念将在下一节中详细介绍。
SSR
当我们规划 Componium 的前端接口时,我们了解了 SSR 的功能和好处。在技术层面上,SSR 需要我们框架的几个部分协同工作才能表现得非常好。以下是一些:
-
组件架构需要支持不同的渲染能力。这包括将组件作为 HTML 预先渲染的能力,这些 HTML 可以通过网络传输到前端。
-
组件需要在服务器和浏览器上都能一致地运行。
-
服务器端渲染的组件应该能够在客户端和服务器环境中获取数据。根据环境的不同,组件应该有类似的数据请求和处理方法。
-
我们需要一个能够结构化和渲染组件的服务器,包括将这些组件的状态和数据附加到它们上。
-
前端应该能够获取组件的服务器端状态,并在以后对其进行激活。它应该能够附加所需的事件。
幸运的是,凭借我们的框架和 Lib 库,我们已经覆盖了这些要求中的许多,并且可以在我们的框架中开发 SSR 支持。使用@lit-labs/ssr包,我们可以在与我们的客户端视图抽象结合的同时定义服务器端渲染。这些功能的模块可以在packages/frontend/client-view.js中找到。为了在我们的框架中拥有灵活的功能集,我们希望开发者使用仅客户端组件和服务器端渲染组件的组合。
要启用 SSR 功能,该框架有一个新的 Renderer 类,其任务是整合所有必需的框架代码和开发者定义的组件。它是通过响应客户端请求,提供一个有效的 HTML 文档的统一模板,并从应用程序目录中注入代码来实现的。为了渲染这些结构,我们可以使用 ssr 库中的 html 辅助函数。要了解 ssr 包的功能,请查看 lit.dev/docs/ssr/overview 上的全面文档。
这些功能与 Componium 服务器端渲染方法协同工作,将生成的 HTML 输出到客户端。一旦 HTML 文档在浏览器中完全加载,则开始水合过程。框架文件加载所需的辅助文件,这些文件将帮助将事件处理器附加到我们的组件上,并使它们具有交互性。
我们将在接下来的 使用框架 部分中检查服务器端渲染功能的详细用法。同时,在我们能够一起使用所有这些功能之前,还有一些架构特性需要介绍。
客户端路由器
在框架规划阶段,路由器被强调为前端基础设施的一个关键部分,它允许界面在应用程序的重要部分之间进行转换。许多前端框架的路由实现非常相似。然而,如果你对路由功能非常热情,React Router 项目 (reactrouter.com) 是一个很好的项目,可以从中学习关于良好的路由抽象、潜在陷阱和路由边缘情况的知识。

图 8.4:初始页面(左)与新路由页面(右)之间的转换
要在我们的框架中启用路由功能,我们将引入一个 ComponiumRouter 类。在 图 8.4 中,我们可以看到初始 /client-render 页面(左)与新路由页面 /page(右)之间的路由转换。路由发生在你点击 this.router = new ComponiumRouter("client-render", routes, this.shadowRoot); 时。
路由器获得一个标识符(client-render),一系列通过 routes 对象定义的路由,以及一个根元素(在这种情况下,是 this.shadowRoot 对象),该元素将用于渲染路由模板。routes 对象被定义为对象数组。例如,图 8.4 的简单路由示例如下所示:
const routes = [{
path: "/client-render",
template: "<nested-component></nested-component>",
},
{
path: "/page",
template: "<navigated-component>
</navigated-component>",
},];
/client-render路由是我们具有客户端视图类型的入口点之一,而/page路由是我们可以导航到的视图。通过在template属性中接受更复杂的模板结构,路由器可以支持更丰富的功能集。例如,我们可以在路由定义中使用来自Componium/Lit模块的html辅助函数来生成更广泛的模板对象。
在导航不同路由之间,我们将依赖于浏览器中更多的内置 Web API。例如,当视图需要更改时,组件会调用history.pushState(...)。在ComponiumRouter类内部,框架处理这些pushState事件并渲染相应的模板。
额外阅读
MDN 文档概述了我们可以在前端路由组件中使用所有可能的 History API 方法。它在这里可用:developer.mozilla.org/en-US/docs/Web/API/History/pushState。
在 Lit Labs 的开源代码库中,还有一个组件路由的例子。它的源代码可以在github.com/lit/lit/tree/main/packages/labs/router找到。在Componium框架中实现路由可以是一个极好的练习。
优化
我们前端功能的目标之一是一组优化,以使我们的框架产生的应用程序更加高效、可扩展和性能良好。我们将通过引入一些功能来优化生产环境中前端组件的输出,朝着这个目标迈出一步。
我们将引入一个新的Optimize类,它包含一些对代码库进行优化的函数。这个类可以在框架目录的packages/frontend/optimize.js中找到。这些优化可以影响包含/注入的框架文件以及应用程序代码。当应用程序在定义了NODE_ENV=production变量的环境中部署时,这个类的函数会激活,这是基于 Node.js 项目的常见模式。
我们将利用一些现有的 JavaScript 工具——在这个特定案例中,是esbuild——来minify框架和组件代码文件。esbuild工具提供了以下压缩命令行 API:
esbuild ${filePath} --minify --outfile=${newFilePath}.
我们可以利用esbuild工具的力量来优化使用我们框架构建的应用程序。例如,当需要时,应用程序中使用的所有组件将由服务器进程进行压缩。在底层,框架会解析组件文件并运行压缩步骤,将优化后的文件输出到单独的目录中。我们使用一个名为.componium的隐藏目录作为存储来保存优化后的文件。框架随后会知道访问优化后的文件而不是原始文件。
为了进一步扩展未来的改进,我们可以将优化重点放在其他应用程序文件上,例如图像、媒体等。引入更复杂的构建工具也是可能的。例如,我们可以添加rollup.js来增强客户端代码的输出结果。我们已经在第三章中看到了 rollup 工具的示例用法。Esbuild也提供了除了最小化之外的额外功能,可以在esbuild.github.io/api找到。
现在,我们需要介绍的是这些前端组件的开发者体验的改进,我们将在下一节中进行说明。
开发者体验
为了完善我们的功能,我们将包括一些改进开发者体验的改进。我们将通过详细记录组件系统以及提供对框架可执行文件的增强来实现这一点。
文档应提供关于前端功能的清晰说明,例如多个客户端视图和静态文件目录的定义。组件结构、可组合性和响应式功能也需要进行描述。例如,这可能包括一个简单的 API 来添加新的客户端路由,以及框架如何使用 Lit 库来实现与组件相关的功能。
框架可执行文件的改进包括使用脚手架操作生成客户端视图路由、视图和组件的能力。在第七章中,我们看到了生成新 API 路由的示例;这是一个非常相似的增加。就像之前一样,使用可执行文件,开发者将能够快速生成一些代码并开始构建前端用户界面。这些预先生成的组件包括对象样式和数据属性的默认属性配置。
总体而言,我们将尽可能解开前端功能的神秘面纱,专注于帮助开发者导航构建客户端 Web 应用程序时所有技术的复杂细节。
依赖项
我们将使用从npm包注册表中获取的几个依赖项来实现本章中描述的功能级别。以下是一些值得注意的依赖项:
-
Esbuild:在框架服务器在生产环境中运行时的优化步骤中使用的打包器和最小化工具。使用Esbuild,我们可以快速优化脚本。它包括许多我们可以用来进一步扩展框架最终脚本的高级功能。 -
Lit:这个库帮助我们扩展现有的 Web 组件技术,并提供了更多高级的组件功能,例如简化数据绑定和简化状态管理。 -
lit-html:与 Lit 库相关的另一个模块,为我们的框架的前端功能提供模板功能。 -
@lit-labs/srr和@lit-labs/ssr-client:这两个模块启用了我们框架的 SSR 功能。它们可以在 Componium 服务器上渲染组件,并在前端进行后续的激活。
主要来说,这些库和工具帮助我们丰富了框架功能。我们的框架可以依赖这些依赖项来启用高效的项目构建、基于组件的架构、动态内容渲染和 SSR,从而实现性能优良、易于维护和用户友好的应用程序。在详细架构就绪之后,在下一节中,我们将探讨开发人员的工作流程,以使用 Componium 的前端功能创建一个简单的客户端应用程序。
使用框架
现在我们已经建立了架构,我们可以通过一个场景来了解开发人员如何使用我们的前端框架组件来构建一个简单的客户端示例应用程序。在第七章中,我们执行了相同的任务,以获得所有功能协同工作以完成特定任务的良好概述。为了跟上进度,请确保你已经安装了依赖项,然后你可以在以下目录中启动示例应用程序:
> cd chapter7/componium/tests/sample
> componium dev
Executing (default): SELECT 1+1 AS result
Componium Server (PID: 59938) started on port: 9000
你将能够打开浏览器,在http://localhost:9000查看应用程序。

图 8.5:一个示例客户端应用程序
根据图 8.4,我们想要创建一个名为框架列表的页面。我们还想在这个页面上列出一些框架。每个框架条目都应该连接到客户端视图,并为每个框架显示一定数量的星星点。除了静态功能外,每个框架组件下还有按钮,可以增加计数器,在这种情况下,我们将计数器标记为每个项目的星星数量。开发人员用例还包括使用 SSR 功能从服务器渲染页面,以提高性能和 SEO。
首先,我们的 Componium 框架允许我们从服务器创建ClientViews。我们可以在app.js文件的应用程序根目录中创建一个客户端视图。我们还可以在这里使用componium可执行文件来搭建组件:
server.addClientView("/", "frameworks", {
title: "Frameworks",
});
以下代码从我们应用的根目录开始,将在 Componium 服务器上指定以/结尾的端点处建立一个托管路由。除了提供路径外,我们还指定了视图的名称以及该视图的附加选项。视图的名称映射到我们应用中的views目录。views/frameworks.js文件定义了我们的服务器端组件处理器:
export default function frameworks(request, response) {
const hitsComponium = 1000;
// code omitted
return `
<div class="framework-list">
<framework-item name="Componium" count=
"${hitsComponium}"></framework-item>
<!-- extra code omitted -->`;
}
要查看此组件的完整版本,请查看tests/sample/views/frameworks.js文件。前面的代码仅列出了列表中的一个框架,但示例中包含了所有必需的项目。进一步查看代码,我们可以在导出的frameworks函数中访问request和response对象。这就是开发者也可以在此文件中访问数据库 ORM 方法以获取数据和预先填充组件状态的地方。
在定义了framework-item组件之后,我们可以启动应用程序服务器并导航到应用程序的根目录。如果我们查看文件源代码,我们可以看到 SSR 组件。部分源代码可能如下所示:
<h1 style="font-family: sans-serif; text-align:
sscenter;">List of frameworks</h1>
<div class="framework-list">
<framework-item
name="Componium"
count="1000"
></framework-item>
组件的状态,包括星星的数量,是从渲染的组件中恢复的。如果我们使用framework-item组件中的以下button元素,那么我们将根据从服务器起源的状态增加星星的数量:
<button @click=${() => this.count++}>Add stars</button>
最后,在创建了frameworks.js应用程序视图之后,我们现在可以将应用程序部署以进行测试运行。在此,开发者应配置app.js进程,使其在开启NODE_ENV=production环境变量的情况下运行。这将启用esbuild优化功能,允许服务器进程压缩我们新创建的组件。
此框架的示例用法包括创建组件、在服务器端渲染它们,并通过点击它们的 UI 元素与其状态交互。这个实际示例以及来自客户端路由器部分的路线示例展示了我们为这个框架功能的初始版本开发的大多数功能。从现在开始的下一步可能涉及找到改进基于组件的架构的方法,以及找到更多为使用我们的框架构建的应用程序添加潜在优化的方法。
摘要
在本章中,我们专注于构建前端架构并向现有项目添加前端功能。类似于在第七章中对服务器端架构的定义,在本章中,我们必须定义前端功能背后的目标,关注开发者希望如何使用我们的全栈框架。我们已经涵盖了定义客户端路由的入口点、响应式概念、复杂组件结构、SSR、路由、优化等主题。前端功能集可能令人眼花缭乱,有很多术语,而且本章之外还有更多内容需要学习。
如果我们将过去几章中我们设计的所有组件组合起来,我们现在最终得到一个包含三个用例的框架,这些用例组合成一个更大的全栈叙事。到目前为止,我们看到了一个 JavaScript 测试框架、一个后端框架,最后是一个位于相同逻辑命名空间下的前端框架。
在下一章中,我们将通过揭示随着框架的发展可能出现的各种情况,重点关注框架维护的必要话题。
第三部分:维护你的项目
总结来说,最后两章深入探讨了 JavaScript 编程空间中框架项目的维护方面和未来。这些章节背后的推动因素是确保开发者构建的项目能够长期使用和可用,从而保证创作的可靠性和有效性。回顾这些系统的最佳实践,最后一章提出了与现有和新项目都相关的当前和未来想法。
在本部分,我们涵盖了以下章节:
-
第九章,框架维护
-
第十章,最佳实践
第九章:框架维护
在我们的框架开发之旅中,我们已经到达了可以详细讨论框架维护的点。到目前为止,我们在前几章中已经完成了全栈框架的初始版本。
在本章中,框架维护主题将引导你走向未来要开发的框架。这些是与框架发布流程、持续开发周期以及最终大型框架项目的长期维护相关的话题。关于框架维护的更详细内容,我们将学习以下内容:
-
开发周期:我们将了解有助于开发正式功能定义的概念和范式,并找出如何寻求框架用户反馈。首先,我们将回顾框架开发过程的分析和设计步骤。然后,我们将转向正式功能定义,以及寻求用户反馈和让这些用户了解框架变化的方法。精通这些概念将帮助你保持你的框架相关、用户友好和具有竞争力。
-
发布流程:熟悉发布流程涉及多个后勤任务,例如定义版本和许可条款,并结合一个能够发布和向公众交付项目的管道。这包括创建自动化管道来构建、测试和部署你的框架。这个过程还教会你如何从代码完成到用户可以轻松采用并从中受益的已发布产品平稳过渡。
-
长期维护:这部分围绕超越典型编码任务的更广泛方面。长期框架管理任务包括监控项目的健康状况、保持其安全性、管理依赖关系以及进行必要的改进。
理解这三个方面——开发周期、发布流程和长期维护——将显著提高你构建和维护框架的成功率。这将确保你的项目保持相关性、安全性和稳健性,并达到最高质量,最终导致其利益相关者获得更好的响应。
技术要求
就像在其他章节中一样,我们将继续使用github.com/PacktPublishing/Building-Your-Own-JavaScript-Framework的仓库。本章的chapter9目录包含几个展示与框架维护相关的工具的示例项目。其中大部分具有类似常见 Node.js 项目的结构。对于chapter9中的每个子目录,你可以探索维护工具示例。有关包含项目的额外信息,请参阅chapter9/README.md文件。
开发周期
为了更好地理解框架维护任务,我们将依靠软件开发生命周期(SDLC)来划分我们框架开发的一些重要里程碑,例如构建功能和向利益相关者发布。我们的目标是将 SDLC 的广泛主题应用于我们的框架项目,重点关注有助于你构建更好项目的具体示例。

图 9.1:软件开发生命周期
图 9.1 展示了软件开发生命周期(SDLC)的简化图,我们可以将其应用于我们的框架项目。在这本书中,我们已经详细介绍了这些步骤,除了我们需要进一步关注部署和维护步骤以完成整个周期。我们已经通过了解其他框架的组织结构和规划 Componium 全栈框架的每个大型部分来处理第一个分析步骤。我们在 第二章和第三章 中对现有的抽象、流行的模式和 Componium 的框架类型进行了有用的考察。为了进一步强化这个分析步骤,我们与潜在的利益相关者和 Componium 测试、后端和前端部分的功能集进行了头脑风暴。在本章中,我们将探讨其他有助于框架开发者使框架项目更成功的策略。
关于 SDLC
软件开发生命周期(SDLC),在某些情况下也称为系统开发生命周期,是一个起源于 20 世纪 60 年代的术语。当时,它被用来帮助开发复杂的企业系统。后来,它被进一步用于软件项目,并结合了如瀑布模型等不同的开发模型。这个术语定义了构建大型系统和软件的系统化过程,为开发过程提供了方法论,并关注质量和编程效率。
软件开发者通常将此模型作为基础,并根据他们在组织中的工作效果来调整构建软件的方法。敏捷软件开发方法的引入是 SDLC 方法的一种扩展。在 JavaScript 应用程序开发的情况下,开发团队可以使用这些生命周期概念来构建新项目,并使用框架工具和库。同时,框架开发者也可以依靠 SDLC 方法来打造高质量、高性能的框架体验。
设计阶段,在分析步骤之后,是我们讨论在第六章、第七章和第八章中构建框架时的情况,它概述了框架的架构部分。这是框架作者专注于弥合需求与编写代码开始之间的差距的地方。当与 JavaScript 生态系统一起工作时,设计步骤尤其有趣。由于 Web 开发的多元化性质,包括 JavaScript 的不同方面、运行时和 Web API,有机会创造创新的设计方法,结合现有和新建的组件。例如,在第六章、第七章和第八章中,我们必须就抽象的组合以及我们打算在全栈框架中使用哪些库做出谨慎的决定。超越库的选择,设计阶段的元素需要就将在框架项目中使用的技术的组合以及这些技术如何相互通信做出具体决定。这就是我们在强烈的观点和灵活的框架之间寻求平衡的地方。
然而,整个 SDLC 过程的概念是一个循环,这意味着设计决策将以循环的方式返回给开发者。这表明并非所有决策都是一成不变的,并且随着项目的进展,随着时间的推移可以改变。然而,有些决策比其他决策更难改变。例如,在Componium测试框架中更改 API 结构将要求所有框架的使用者重写或迁移他们的测试到一个新的 API。很可能会发现,API 更改是为了框架及其用户的利益,但它仍然为使用框架的项目创造了摩擦。这可能导致框架用户被困在项目的某些版本上,或者完全转向使用其他东西。
作为框架开发者,在项目初始发布后,你将再次达到设计阶段。当这种情况发生时,框架的生命周期管理(SDLC)将更加专注于功能开发和代码的大规模重构。在现实世界中,这个阶段的例子可以在 Vue.js 项目中找到。Vue.js 的主要版本引入了对渲染器的更改,使得与模板指令一起工作变得更加容易,并改变了组件的 API。关于这次迁移的更多细节可以在v3-migration.vuejs.org/breaking-changes找到。在这些案例中,框架作者会回到起点或找到方法来彻底重新设计框架的某些面向公众和底层结构。
这种回到设计阶段的周期在框架的演变中很常见,尤其是在它处于活跃开发或拥有繁荣的用户基础时。虽然这可能对现有用户造成干扰,但这个迭代过程是框架长期成功和可持续性的关键。未能适应或演变的框架可能会迅速过时并失去相关性。以 Angular 框架项目的演变为例。在从 AngularJS 到现代 Angular 版本的演变过程中,它进行了完整的重写。虽然最初对开发者造成干扰,但这种过渡使框架能够现代化其架构并利用最新的 JavaScript 功能。它还奠定了更未来化的增长路径的基础,确保 Angular 在顶级前端 JavaScript 框架中的地位。
另一个设计阶段会重复出现的地方是性能改进的引入,特别是那些影响 JavaScript 项目的改进。鉴于 JavaScript 语言的本质和不断发展的网络技术,性能优化通常是一个持续的任务。React 库背后的团队一直不断回顾其设计阶段以进行性能改进,这导致了诸如引入 Hooks、并发模式和新的 JSX 转换等重大改进。SDLC 的设计阶段让你忙碌不已,因为它是一种持续的活动,使框架能够保持相关性、性能和对其用户的实用性。关键是如此处理这些设计更改,以最大限度地减少干扰并为用户提供清晰的路径以适应这些变化。继续探讨类似的主题,在接下来的章节中,我们将了解框架作者可以采取哪些措施来安全地实施和维护新功能。
寻求用户反馈
框架开发中最激动人心的部分之一是让一些利益相关者使用你所开发的内容。这些利益相关者可能是你公司内部的团队,外部开源用户,甚至是你的团队本身。你生产的框架产品需要一个高效的反馈循环。这个循环将包括找到从尽可能多的利益相关者那里获取最佳反馈的方法。在成功获取所需输入后,框架开发者可以概述所需的更改以适应这些反馈。这些更改可能包括修复问题、添加新功能、改进开发者体验或整体组织调整。一旦这些更改到位,我们就达到了框架开发过程中的另一个激动人心的部分——向用户交付框架的新更新版本。
在本章的这一部分,让我们专注于寻求用户反馈的第一部分,即正确收集反馈。根据您项目和组织的规模,您可能需要与大量利益相关者互动,或者只是与少数用户进行一对一的互动。对于较大的群体,利用一个可以促进讨论并能够就框架的各个方面进行不同讨论的系统是有益的。最简单的方法是维护一个问题或功能请求跟踪系统。这与其他软件项目非常相似,但在框架中,在管理该系统的各个方面投入更多时间是更加重要的。使用框架,您希望能够清楚地分离问题和功能讨论,为项目的不同版本安排反馈。
该框架还可以提供将反馈直接发送到正确位置的机会。例如,收集开发者反馈可以嵌入到项目的开发者体验中。这就是开发者日常使用的可执行文件可以提供链接到功能请求跟踪器的地方。在第七章中,componium可执行文件具有生成某些常见组件脚手架的机制,并且反馈方向可以直接集成到该可执行文件中。总的来说,你必须依赖工具和系统来收集反馈,避免花费太多时间手动管理所有这些反馈。最重要的是,尊重所有类型的反馈非常重要。在某些情况下,框架内构建和描述的功能可能不适合某些利益相关者。因此,欣赏每一份反馈作为独特的视角,它带来了创新和项目扩展的前景是至关重要的。在下一部分,我们将讨论一个在开源和私有框架项目中都取得良好效果的正式特征定义过程。
正式特征定义
请求评论(RFC)和请求提案(RFP)的过程在许多行业中普遍用于定义新功能或获取反馈。这些相同的流程在软件框架开发中也同样适用。许多框架已经实施了他们自己的正式功能定义流程,并内置了利益相关者的反馈。例如,Ember.js 在其 RFCs 托管在github.com/emberjs/rfcs,Vue.js 在其 RFCs 托管在github.com/vuejs/rfcs,跟踪所有新、待定和归档的关于功能添加和项目重大变更的提案。由于这些框架的开源性质,任何人都可以在那些存储库中就新功能提案发表意见。这些提案以与代码贡献类似的方式进行审查。方便的是,这些功能定义存储在代码存储库中,以便将来进行记录和引用。
包括正式功能定义的过程有助于组织框架开发者和用户参与框架开发生命周期(FDLC)的设计阶段。作为框架开发者,你可以自由选择参与这个过程的程度。例如,如果我们向 Componium 添加了一个新功能以获得原生的 WebSocket 支持,我们可以创建一个 RFC 来解释该功能的要点,提出相应的 API,并说明这对整个全栈框架将是有益的。根据我们组织的复杂性,添加这个特定功能的提案可以经过以下阶段:
-
提议: 当新的 WebSocket 功能被详细描述时。在这个阶段,其他框架开发者和潜在的利益相关者有机会提供初步反馈。在我们的例子中,这是提出公共 API、前端和后端集成以及 WebSocket 功能覆盖的地方。
-
探索: 框架开发者可以探索技术原型并探索该功能的潜在架构的阶段。在这个阶段,可以实现细节可以细化。对于框架项目来说,与对框架有利益关系的团队分享该功能以寻求进一步反馈也是一个好主意。
-
接受: 如果该功能被认为足够完善,那么框架可以在此阶段开始实施,并将代码合并到主代码库中。
-
准备发布: 预发布阶段是创建框架最新改进的发布候选版(RC)的好机会。这也是一个获取有关该功能如何与现有项目和集成工作的有用反馈的机会。在维护方面,可以引入用户文档。
-
发布:最终阶段是功能发布并可供使用的时候。这是 RFC 最终被标记为完成并发布的时刻。未来的提案也可以参考这个功能,以帮助提供反馈和技术架构。
有这样一个正式的过程有助于构建一个有组织的功能开发和方法,然而,值得注意的是,许多功能可能会在提案和探索阶段停滞不前。
一个简单 RFC 的实例,它被接受并合并,是移除 Vue.js 对 Internet Explorer 11 的支持:github.com/vuejs/rfcs/pull/294/files。文档中提到了改变背后的动机,包括对维护负担的担忧以及这种改变如何影响框架的使用者。该提案的讨论线程中贡献了超过 50 条回复,包括框架核心团队的成员以及其他热衷于这一改变的开发商。
RFP 流程可以以类似的方式触发其他实体的兴趣或投标,以帮助贡献到框架中。如果你想要添加到你的框架中的某个功能,但无法做到,那么通过这个过程,你可以找到一个愿意为你做的供应商。这个过程在企业专有环境中更为常见。
这两种方法都在功能开发周围创建了一个结构,并帮助我们以适合自己的方式遵循 SDLC。随着你的框架发展和演变,你可能会选择调整这些正式的功能定义过程,以最好地满足你的需求。
在建立功能开发周期之后,在下一节中,我们现在可以强调 JavaScript 框架如何处理发布流程,包括提供报告给其利益相关者关于新版本中所有新改进和功能的能力。
发布流程
在图 9.1 的 SDLC 图中,部署步骤表示软件可以被消费者使用的时候。在框架开发的背景下,这意味着一个新版本发布。这是新创建的功能变得可用的时候,为了使它们可用,开发者需要通过发布流程**。在这一章节的这一部分,我们将探讨与 JavaScript 框架项目的初始和后续发布相关的主题。这包括展示一些现有的工具、许可选项、版本控制和持续交付。为了遵循开发周期部分,其中我们讨论了新功能的引入,我们将首先学习如何让每个人了解框架的变化。稍后,简化发布*部分将讨论使发布任务更易于处理的机会。
变更日志
框架开发者已经需要花费大量时间来规划、设计和开发功能。幸运的是,对于发布这些功能的最后一步,收集所有新构建框架组件的详细信息的过程可以标准化和自动化。为了实现这一点,项目依赖于现有的工具,如发布实用程序和变更日志生成工具,以支持框架的新版本发布。首先需要维护和生成的是每个发布中变更的日志。变更日志在任意框架设置中都很有用,并且应该是您与利益相关者沟通每个新发布影响的途径。
这里有一些来自其他流行项目的变更日志结构的好例子:
-
Electron:应用程序框架项目在其releases.electronjs.org/releases/stable上托管变更日志,提供了关于稳定发布和即将发布的预发布变更的非常详细的信息。它允许您通过主要框架版本进行筛选,并通过直接链接到 GitHub 上的源代码来集成已提交的代码。在releases.electronjs.org/history上的发布日历也提供了所有新标记版本的直观展示。这个例子对于新创建的框架来说可能有些过度,但对于拥有大量用户的成熟项目来说效果很好。
-
changesets自动化工具。 -
Next.js:该框架使用GitHub Releases功能概述github.com/vercel/next.js/的变更。使用 GitHub 的界面允许项目在同一页面上记录变更,包括贡献者、资产和源代码。GitHub 的工具允许您在发布之间进行对比,以获取一系列从上一个发布中发生变更的提交。变更日志本身的创建是在框架内部的github.com/vercel/next.js/blob/canary/release.js中使用手动脚本完成的。当框架维护者触发时,这些脚本通过GitHub Actions进行自动化。
这些变更日志的共同主题是,通常情况下,变更日志是根据某些代码提交信息结构自动生成的,并使用不同的自动化工具发布,供用户浏览。根据您的需求,您可以从以下工具或类似工具中受益,在您的 JavaScript 框架项目中:
-
<type>(<scope>): <summary>。这些约定根据框架开发者的选择,要么变得更加复杂,要么变得更加简单。 -
commitlint (github.com/conventional-changelog/commitlint):另一个帮助验证提交信息结构和遵循特定提交约定的工具。它能够通过交互式 CLI 或程序性使用验证字符串形式的消息。
-
Changesets (github.com/changesets/changesets):为简单和多个包的代码库生成更改集的更复杂解决方案。当框架分散在多个来源时,这很有帮助。
在您考虑使用这些工具为您的框架项目服务之后,您会发现它们之间有许多共同的主题。大多数时候,这些工具在配置和结构方面有不同的方法。然而,在众多选项中,使用其中任何一个都可以通过向用户提供有关框架进度的关键信息来帮助您节省时间。例如,Ember.js 项目有一个易于遵循的变更日志(github.com/emberjs/ember.js/blob/main/CHANGELOG.md),这有助于开发者跟上更新。如果您探索一些变更日志,包括 Ember.js 的,您将看到在整个开发周期中维护的不同类型的版本控制,这正是我们将在下一节中讨论的内容。
版本控制
为您的框架维护适当的版本控制可能是一个挑战。保持适当的版本控制方案有助于利益相关者理解其代码与新框架迭代版本的兼容性。如果一个项目版本控制得当且严格定义其版本,那么下游消费者在更新框架依赖项时可以充满信心。上游框架版本中的任何变化都可能导致现有组件损坏,并在已构建的应用程序中造成混乱。在许多情况下,尤其是在较旧的软件中,框架用户被要求在已知项目存在安全漏洞的情况下谨慎更新到框架的最新版本。一些例子包括从 AngularJS 到现代 Angular 版本的剧烈变化,或者 JavaScript 工具(如 webpack)的主要版本变化。此外,如果一个项目经常违反版本控制协议,那么框架的用户将减缓他们的升级周期,并可能在未来避免使用特定的项目。
幸运的是,软件社区已经围绕版本控制创建了标准,如 SemVer (semver.org) 和 Calendar Versioning (CalVer) (calver.org),这有助于定义适当的版本控制协议。这些标准可以帮助定义框架发布流程,并且应该在框架文档的某个地方进行记录。

图 9.2:语义化版本控制
语义版本控制,如图 9.2 所示,是目前许多项目中采用的最常见的系统。它由三个必需和一个可选组件组成:
-
首先,我们有 主版本,这表示如果用户从早期主版本更新,则这些更改是不兼容的
-
次要版本意味着有新功能可用,并且发布仍然与早期版本向后兼容
-
最后必需的组件是 补丁版本,这意味着有修复的 bug,这些修复也与早期版本向后兼容
-
版本末尾的可选组件可以包括预发布名称,以及添加元数据或构建号
semantic-release (npmjs.com/package/semantic-release) 包使用 SemVer,并尝试通过利用代码库提交历史来简化 JavaScript 项目的版本管理。它作为一个实用工具在自动化发布步骤中工作。
可选的 CalVer 格式使用 YYYY.MM.DD 格式的日期来定义其结构。如果项目发布基于日历年份,这可能很有用。最终,框架维护者决定版本集,正确设置这些版本并确保下游用户的应用程序构建不受破坏是一项重大责任。
简化发布
到目前为止,我们已经看到了许多自动化发布过程不同部分的工具。我们专注于版本管理、功能反馈和变更日志。我们可以引入工具进一步简化我们框架的新发布工作流程。这类工具旨在确保所有发布过程任务都成功执行,并要求发布在各方面保持一致。

图 9.3:发布新版本
如 release-it (github.com/release-it/release-it) 和 np (github.com/sindresorhus/np) 这样的工具可以使发布过程顺利进行。图 9.3 展示了在发布 Componium 测试的新版本时 np 工具的作用。这些工具确保为您的项目完成以下任务:
-
执行任何先决脚本,这可能包括格式化和检查文件。
-
必需的发布测试运行并通过。如果测试失败,则发布过程被中止。
-
使用维护者选择或基于某些其他标准(例如使用我们之前看到的 semantic-release 包)来提升版本号。
-
将代码发布到特定的注册表。对于内部项目,这可能是一个内部源;对于公共项目,这意味着将源代码上传到公共注册表。这很可能是框架用户获取最新代码的来源。
-
将必要的标签推送到与注册表中发布的版本号相同的代码仓库。
这些只是一般运行的一些步骤,但对于更复杂的项目,许多步骤可以根据代码库的需求进行调整。这些工具的使用取决于它们如何融入你的工作流程。从简单开始并找到即插即用的工具是个好主意。随着框架项目的增长,你会发现自己在混合和匹配不同的工具,以形成自己独特的发布流程方法。例如,第三章提到了用于内部 Angular 开发的ng-dev工具。在该工具的内部,团队使用np命令行工具进行发布流程。release-it包提供了一些额外功能,适用于生活在单一仓库代码库内或需要进一步配置的项目。
维护工具展示
chapter9中的书籍代码仓库包含了一组你可以快速尝试并查看其有效性的工具。你的框架项目可以集成包含的工具或类似工具,从而提高框架开发工作流程。chapter9/commitizen目录包含了一个使用 Commitizen 包来强制执行项目 Git 提交指南的项目。
maintenance-tools目录展示了用于框架维护的几个 Node.js 实用工具。要查看可用的脚本,请确保运行npm install然后npm run dev。
在下一节中,我们将达到发布流程的最终里程碑,即结合我们迄今为止所看到的工具和持续集成环境,通过单次点击按钮即可发布框架的新版本。
持续交付
配置和维护框架发布及其他任务的基础设施涉及遵循最佳实践和采用 DevOps 方法论空间中的工具。这包括混合软件开发任务和 IT 运营以提高软件发布,使它们更容易管理。通常,与 DevOps 系统集成将需要学习除你在开发的框架中使用的核心技术之外的新技术技能。这包括了解自动化最新方法、安全发布流程以及在 DevOps 环境中的应用部署。
现在,为软件项目设置 持续集成(CI)步骤变得无处不在且毫不费力,对于框架项目来说,这样做非常重要。使用 CI 环境确保所需的框架测试在隔离环境中运行非常重要。CI 步骤还确保代码质量并有助于创建良好的框架开发工作流程。持续交付(CD)管道旨在交付框架产品。它与 CI 步骤一起配置,以准备要检查、构建和测试的代码更改。这些管道也在开源和内部环境中进行配置。
交付部分确保维护者可以准备项目的新的发布版本,这包括执行简化发布流程中的一部分工具。在交付阶段,内部开发脚本可以运行所有与发布相关的任务,这可能包括生成项目文档和发布其他工件。这个发布环境也有权限访问发布代码到相关注册表的必要凭证。在交付阶段,维护者可以配置我们在本章中看到的所有不同类型的工具,以自动化发布框架软件新版本的过程。
例如,可以使用 GitHub Actions 设置工作流程来发布 Next.js 的新版本。这些工作流程可以在 github.com/vercel/next.js/actions/workflows/trigger_release.yml 上看到执行,由框架的维护者触发。配置这些自动化工作流程将大大提高你的生产力,因为它将避免许多手动任务。这也会让你对自己的产品更有信心,因为这些工作流程为所有框架维护者设定了高质量的标准。
持续集成示例
与 简化发布 部分中分享的维护工具信息类似,你可以在 chapter9/ci 目录中找到一个 CI 配置的示例。这个配置可以用作你自己的项目,配合 GitHub Actions (github.com/features/actions) 和 Circle CI ([circleci.com]) 基础设施。
要测试这些配置,你可以将章节中的文件复制到自己的仓库中,在那里你可以完全访问并编辑项目的源代码。
发布的另一个相关方面是许可,其中框架作者需要解释他们创作的使用和分发条款。这就是我们将在下一节讨论的内容。
许可
随着您的框架发展,您会发现您需要为代码库设置一个适当的许可。此发布程序可以适用于内部开发项目和开源倡议。在两种情况下,您都可以从许多不同类型的许可中选择。对于私人商业相关项目,您可以选择一个专有许可,限制框架在公司控制之外的使用。这种许可赋予代码库和内部项目独家所有权。如果您想出售或仅限制向为使用您的代码付费的用户重新分发,商业许可可能很有用。例如,您可以在一些 JavaScript 框架中找到它们分发框架的不同版本,如Sencha Ext JS (store.sencha.com),它包括社区和企业版本。扩展的企业版本可以包括更多支持、定制功能和专门的开发者关注。
对于开源用例,也存在一些许可选项。位于 choosealicense.com 的网站支持软件开发者,并帮助他们确定开源其工作的需求。您会发现许多流行的开源项目使用以下许可:
-
MIT 许可:这是一个非常宽松的许可,允许商业使用、分发、修改和私人使用。它基于您保留版权声明并避免从您的代码中承担任何责任或保证的条件。
-
GNU 通用公共许可证(GPL):这是一个 copyleft 许可,提供了与 MIT 许可类似的商业权限,但在专利和分发规则方面更为详细。然而,使用此许可时,存在披露源代码的条件。
-
Apache License 2.0 许可:这是一个类似于 MIT 的宽松许可,但在商标和专利使用案例方面有额外的限制。如果任何人根据 Apache 许可更改了您框架的代码,那么他们需要声明这些更改。
作为框架的作者,在决定所有贡献者在向项目贡献时应遵守的许可时非常重要。在将来更改许可的过程需要相当多的努力,因为所有贡献者都必须在新许可下重新许可他们的代码。同时,记住您在框架中使用的库的许可类型也很重要。
这部分关于发布流程项目的讨论就此结束,其中包括收集反馈、通知用户新版本发布以及帮助优化这些发布。现在,我们准备继续讨论其他长期维护的主题。
长期维护
到目前为止,在本章中,我们已经探讨了随着框架在开发生命周期中进展而发生的维护任务,涵盖了关于初始或后续功能更新的主题。然而,框架开发还有其他一些独特的方面,它们是长期维护的一部分。为了集中讨论几个方面,我们将探讨安全、依赖管理、功能兼容性演变等主题。
安全
近年来,Web 应用程序安全的方法已经发生了变化。现在,在安全领域有更多工具和解决方案,试图保护整个开发工作流程。当用户选择一个框架来满足他们的需求时,他们也会对它有一定的安全期望,尤其是如果框架是为处理关键数据和用户输入而构建的。在维护你的框架时,你可以期待收到解决安全漏洞的漏洞和补丁。像HackerOne(hackerone.com)和Huntr(huntr.dev)这样的漏洞赏金计划网站专注于保护软件,并且可以就针对你的框架提交的漏洞报告与你联系。内部和开源框架都可以收到报告,作为维护者,你的期望是修复已知的漏洞,以保持强大的安全态势。
创建的漏洞可以被分配一个通用漏洞和暴露(CVE)标识符。例如,看看 Electron 的 CVE-2022-29247(nvd.nist.gov/vuln/detail/CVE-2022-29247),它报告了框架进程通信中的漏洞。它概述了框架的修复版本和风险评分。
为了保持攻势并降低漏洞风险,你可以遵循以下策略:
-
文档化危险 API:投入时间编写文档,以突出可能在使用不当时造成危险的 API。在服务器框架中,这可能包括解释如何保护免受危险请求数据负载的影响。在前端,问题可能源于不安全地渲染 HTML 或未能清理 URL 或其他类型的输入。例如,Vue.js 项目有一个最佳实践指南,其中包含有关此主题的信息:vuejs.org/guide/best-practices/security.html#what-vue-does-to-protect-you。此策略也适用于非应用程序框架。
-
安全审计:这类审计可以帮助对框架进行常见攻击向量或特定漏洞的测试,这些漏洞可能会影响框架的功能集。在这个过程中,你的代码可以被内部安全团队或第三方审计,以发现潜在问题。目标是找到即使在使用得当的情况下也可能导致框架造成损害的攻击向量。对于应用级框架,存在OWASP 应用安全验证标准(ASVS),它概述了 70 多页的技术安全控制,以确保安全开发。这些控制可以在多种语言中找到,见owasp.org/www-project-application-security-verification-standard。
-
更新依赖项:依赖外部模块和库会在底层代码中发现漏洞时引入安全风险。从我们在 Componium 和其他 JavaScript 框架中看到的情况来看,项目依赖的外部依赖项非常多。最近,越来越多的安全扫描器,如 Socket (socket.dev) 和 Dependabot (github.com/features/security),已经可用,专门用于追踪 JavaScript 漏洞并通知维护者修复它们。然而,这些扫描器无法修复问题并创建版本,因此框架开发者仍需跟上所有依赖项的更新。
-
SECURITY.md文件,其中记录了安全策略。它通常位于项目的根目录中;express.js项目的安全策略文件位于github.com/expressjs/express/blob/master/Security.md。
在框架安全维护方面,总有太多需要跟踪的内容,但即使投入一点时间来改善安全状况,也能减轻你的负担,并充分提高你的项目效益。跟踪安全任务也与你的项目的依赖项相关。下一节将重点介绍管理可能以不同方式影响你的项目的依赖项,包括项目的安全方面。
依赖项
从长远来看,管理 JavaScript 框架的依赖项可能是一项非常复杂的任务。所依赖的库和模块可能会过时或不再维护,这不仅仅受到安全问题的困扰。随着生态系统的不断发展,框架开发者需要关注一些内部使用的陈旧模块。当悬而未决的 bug 修复依赖于框架代码库之外的组件时,依赖项的更新不足可能会成为限制因素。如果依赖的包完全被废弃,创建自己的副本并尝试修复问题是一个好主意。另一个选择是迁移到类似的包或独立重写它。依赖项也可能以某种方式破坏兼容性,框架作者将需要重构该模块的使用以恢复兼容性。
一个更积极的发展趋势是框架项目内部使用的库中增加了额外的功能。在这种情况下,项目和其用户可以从这些改进中受益。这些增强可能包括新增的令人兴奋的新功能或潜在的性能优化。

图 9.4:运行 npm-check-updates
我们可以依赖某些依赖管理工具来跟踪我们的依赖项。例如,图 9**.4 展示了 ncu 命令行,它追踪了一些有新版本的依赖项。或者,自动化的 CI 工具也可以生成类似的报告。
由于生态系统的特性,跟踪 JavaScript 项目的依赖项尤其困难,因此,无论是通过使用工具还是最小化依赖项的数量,对于任何规模的框架项目来说,制定一定的策略都非常有用。
依赖管理符合功能覆盖的更大主题。随着项目的进展,框架设计者会改变功能的结构,并移除未使用的功能。这是每个维护者需要考虑其框架项目长期战略的问题。
功能覆盖和弃用
在本章的开发周期部分,我们使用了 SDLC 并定义了流程来推动功能开发。从框架项目的长期视角来看,保持对实用功能和任何利益相关者未使用的功能的好覆盖是有用的。当我们考虑用户反馈时,我们还需要确保一个功能从长期维护的角度来看是值得添加的。这就是考虑添加更多功能的快速胜利与项目的长期愿景之间的权衡所在。以类似的方式,废弃框架的功能可能意味着清理不那么相关的组件。通常,这会涉及到为现有用户创建迁移路径并提供替代解决方案的漫长过程。否则,项目会随着时间的推移失去一些信誉。为了避免扩大复杂功能管理的障碍,框架创建了扩展接口,允许在不膨胀核心功能的情况下进行功能扩展。我们在几个项目中看到了这样的例子。例如,Componium 服务器允许基于express.js行为拦截请求的自定义中间件函数。Vue.js 是一个前端框架示例,它提供了一个插件接口,用于功能,这些功能不能捆绑在核心框架中:vuejs.org/guide/reusability/plugins.html。
性能优化是一种通常跨越长期的特征优化。框架可能通过用户反馈或特定用例发现瓶颈或减速。这就是性能倡议,跨越多个发布和大量重构,可以用来开发一个更优化的产品。
在本节中,我们讨论了一些在框架生命周期中可能出现的一些长期问题和任务。其他我们没有涉及到的维护工作可以通过熟悉模式来解决,包括引入特定工具、利用外部服务或依赖现有软件方法来减轻维护负担。
摘要
在本章关于框架维护的内容中,我们学习和回顾了一些新的和熟悉的话题——开发周期、发布流程和维护任务。这三个话题使我们能够成功维护一个 JavaScript 软件项目在长时间内。我们深入研究这些主题的部分原因是为了让您能够根据自己的工具和技术选择创建自己的维护工作流程。
当我们审视开发周期的步骤时,我们将范围缩小到 JavaScript 框架开发的特定性。在此主题旁边,我们学习了 RFC 流程,并找到了从我们框架的用户那里获取有价值反馈的方法。此外,我们关注了发布过程,这包括了解我们如何对版本、许可、文档等方面进行结构化。最后,长期维护任务包括为在其他 JavaScript 项目中先前发生的事件做准备。这些包括依赖管理、处理安全事件以及处理过时功能等话题。
总体而言,我们已经捕捉到了框架维护的精髓,这应该为你探索其他项目中存在的维护方面提供了一个良好的基础。我鼓励你检查其他框架。例如,通过查看第一章中的开源框架,你可以找到那些项目中维护所使用的其他工具和技术示例。
在下一章和最后一章中,我们将回顾本书的所有关键基础,并通过讨论这个主题的最佳实践来结束我们的 JavaScript 开发之旅。
第十章:最佳实践
在本书的最后一章,是时候结束我们的旅程了,我们将探讨围绕通用 JavaScript 框架开发的几个关键主题,并展望这个生态系统框架的未来。在前面的章节中,我们已经分析了现实世界的例子,并建立了一个以项目维护和组织为中心的稳固知识库。利用这些实际知识,在本章中,我们将关注框架的当前状况,并检查几个预测,以了解这个领域未来的创新方向。总体目标是理解目前普遍存在的 JavaScript 框架开发的最佳实践,以及探索一些随着出现而出现的未来模式。
本章的核心要点将围绕弥合我们现在在框架开发空间中的位置与框架作者将在近期到长期未来构建的解决方案之间的差距。这一深入探讨将涵盖以下主题:
-
框架的常见主题:第一部分讨论了我们在本书中看到的许多框架项目中的几个架构模式和常见选择。如模块化、代码库的方法、最佳实践标准化以及基于性能的设计是有效且稳健框架的基石。有了这些元素,我们将更好地预测这个软件开发领域的创新。
-
框架的未来:我们将看到哪些因素将影响框架随时间推移如何演变,重点关注与开发者体验、解决全栈复杂性以及突出潜在的新开发方法相关的主题。本节突出了即将重新定义行业轨迹的开发方法中的潜在范式转变。在开始为公众构建新软件时考虑和研究新趋势和技术是很重要的。
-
额外开发考虑因素:最后,为了结束我们的冒险之旅,关于额外考虑因素的章节将聚焦于诸如时间投入、财务支持以及整体软件支持等重要因素。这些因素在项目进展过程中往往被忽视,但它们对框架项目的进程和结果有重大影响。对于任何类型的框架开发者来说,这些额外的考虑因素都至关重要。
在这些关于最佳实践的主题上,本章突出了 JavaScript 框架开发的持久原则——考虑常见主题,探讨将塑造类似项目未来的因素,以及必须考虑的辅助考虑因素。理解这些最佳实践是解锁您理解和影响自己 JavaScript 框架开发轨迹潜力的关键。这些技能将保持未来性,无论技术环境如何变化,因为您进一步深入到项目开发中。关于开发中常见主题的第一部分探讨了框架开发中今天重用的几个概念示例。让我们开始吧。
技术要求
与第九章类似,chapter10目录包含几个展示框架最佳实践相关工具的示例项目。请参考chapter10/README.md文件,以获取有关这些章节子目录内容的指南。技术要求与其他章节类似,例如使用 Node.js 20 或更高版本。
常见主题
观察 JavaScript 生态系统中的框架项目当前状态,我们可以看到 JavaScript 框架领域的稳定性、活力和混乱。例如,我们看到了许多项目采用在现有原语之上构建的方法,例如许多框架使用 React 组件库作为组件架构和浏览器渲染的基础。在另一端,项目是从零开始创建的,重新发明了浏览器渲染的方法或使用 JavaScript 解决软件开发中的特定挑战。本节探讨了在许多项目中出现的类似常见主题。了解这些特定的共同点有助于框架开发者与整个生态系统保持联系,并开发更一致的项目。
当我们从宏观的角度审视所有这些项目的当前状态时,在光谱的一端,我们发现了一些大型、成熟的框架,它们支撑着众多高流量应用和复杂的工具。随着每一次的发布,这些框架都在增强它们的成熟度和稳定性。例如,Electron 作为最受欢迎的框架,用于利用网络技术进行应用开发,其每个新版本都在稳步提升其设计和性能指标。相反,一系列不断发展的项目在 JavaScript 社区中产生了新的、创新的想法。这些新来者,无论是作为公共资源引入还是为内部业务需求定制,都为生态系统注入了一剂新颖和多功能性。例如,Svelte 和 SvelteKit 挑战了一些既定的范式,并引导某些开发者的思维模式转向不同的方法。随着网络应用架构的方法不断变化和发展,整个光谱都激发了所有 JavaScript 开发者的兴奋、机会和新技术的进步。
在 第一章 的 框架展示 部分中,我们绘制了框架开发在漫长的时间线上的演变。从最初作为特定任务的专用解决方案开始,例如它们最初专注于单页应用,框架已经发展到成为全面发展的开发平台。现代框架的功能呈指数级增长和丰富,包括解决全栈需求以及更多。在 第六章、第七章 和 第八章 中,我们通过 Componium 框架的开发实例,看到了对各种包和抽象的依赖,以构建这个全面的框架,形成一个完整的全栈系统。
模块化
模块化是我们可以从本书中提到的许多项目中提炼出的一个常见主题。模块化概念以多种方式适用于 JavaScript 项目,并且与其它编程生态系统中的项目相比,它们在 JavaScript 项目中具有特殊性。模块化开发方法得益于网络应用的结构以及 npm 等注册表中的包结构,这些结构由 package.json 格式支持。相比之下,当观察像 Python 这样的编程语言时,它们依赖于外部项目依赖,但缺乏一种标准化的方法来有效地处理这些依赖。JavaScript 处于一个独特的位置,其框架使用许多内部和外部模块。这种方法在开发速度上具有益处,但从维护的角度来看,也带来了一定的负担。

图 10.1:AdonisJS 的模块化
图 10.1 展示了 AdonisJS 中模块化的一个示例。该图展示了该框架第一方包的概要结构。AdonisJS 框架的包组织得很好,并且为了开发者的便利而解耦,开发者可以选择更适合他们用例的包。大多数包都是从 Adonis 命名空间安装的——npm install @adonisjs/ally。它们随后使用框架的命令行工具进行配置,称为 Ace。在 AdonisJS 核心中,代码库也依赖于几个模块来开发以及提供最终用户功能:github.com/adonisjs/core/blob/develop/package.json。这仅仅是我们在整个生态系统中的项目中看到的一个常见主题的另一个例子;随着新项目在社区中变得更加流行,这种做法可能不会很快改变。作为一个框架开发者,你会接受现有的代码和结构,并将你的项目组织成模块。
测试模块化
chapter10/adonisjs 目录包含一个示例 AdonisJS 项目。在项目目录中,你可以运行 npm install 然后执行 npm run dev。
一旦项目运行起来,你可以在浏览器中打开以下地址:http://127.0.0.1:3333/。当示例应用运行时,你可以通过 npm install @adonisjs/ally 安装和使用额外的模块,如本节前面列出的模块。你可以在 adonisjs.com 找到更多模块和包。
架构复杂性的增加与模块化的好处形成对比。例如,一个框架消耗或暴露的模块越孤立,你就越需要考虑耦合以及所有这些模块如何协同工作。最近的一些挑战涉及保持依赖项更新或标记框架分离模块的新版本。框架开发者必须投入更多时间来管理他们消耗和产生的依赖项,这在 JavaScript 平台上在不久的将来不太可能改变。
设计演变
许多 JavaScript 框架中一个常见的主题是设计演化的概念。与其他语言中的系统类似,用 JavaScript 构建的框架需要随着时间的推移而演化,以应对不断变化的环境。变化因素可能包括技术变革或进步、新的行业趋势,或者赶上竞争框架。在 JavaScript 的情况下,这些因素包括对网络浏览器、Node.js API、运行时改进等方面的进步。明确的抽象和深思熟虑的架构可以帮助您适应这些变化,而无需在框架项目中进行剧烈的重构。一些例子可以证明在 JavaScript 框架存在的环境中发生了这样的剧烈变化,例如前端框架中引入了 Web 组件和相关现代 API。一些框架选择接受新的标准或与之集成,以更好地适应不断演化的网络。
另一个例子是迁移到 ECMAScript 模块(ESM)格式。最初,项目必须适应第三方模块系统,如 CommonJS 或 AMD,或者实现自己的系统。然后,随着官方 JavaScript 模块定义的创建,项目必须接受新的代码库结构方式。尽管 ESM 带来了诸如静态模块结构和在所有 JavaScript 环境中改进的语法等好处,但在某些用例中仍然存在复杂的兼容性问题。框架作者需要确定并评估其项目对 ESM 的支持。例如,从版本 16 开始,Angular 项目开始支持作为开发者预览的 ESM 模块,并将其作为一个选项提供。这使得项目能够通过动态导入表达式和懒加载模块来扩展功能集。此外,这一变化还提高了应用程序构建的构建时间性能。它还允许框架使用更现代的工具,如 esbuild,该工具也被用于 Componium 框架。

图 10.2:Electron.js 的部分发布说明
对于像 Electron 这样的应用程序框架,框架功能在每个主要版本发布时都会有所变化。这是因为主要版本通常会跟踪底层 Chromium、Node.js 和 V8 的变化,如 图 10.2 中的发布说明所示。随着这些组件中新的修复和功能的引入,框架提供的内容会随着新版本的发布而演化。这是一个令人信服的例子,说明了项目如何利用外部依赖的持续演化的改进。
新增了许多框架的新功能,如服务器端渲染,因为出现了新的在服务器端加湿和生成视图的方法。现有项目添加了新功能,以便服务器端渲染等特性能够适应现有的 API 界面和架构。从整体上看,我们可以在各种类型的项目中看到这些类似的趋势变化。深思熟虑地适应最新趋势可以避免项目停滞,并使我们能够跟上最新的 JavaScript 进展。
极简主义方法
另一个常见的主题是框架设计的极简主义方法。一些框架可能选择专注于简洁和最小化的架构变更。在这些情况下,依赖项的数量和复杂性都大大降低。更简单的框架在资源有限的环境和不需要大量框架开销的项目中可能非常有效。在 JavaScript 项目中,这些框架通常旨在提供一个简单的 API,文件大小小巧,主要关注特定的独立功能集。如果功能集符合要求,选择这种类型的方法可以减少框架开发所需的资源,并为利益相关者提供一个更加干净、简单的界面。
为了突出一些例子,从前端的角度来看,Preact (preactjs.com)作为一个库,采取了一种极简主义方法,为 React 库提供了一个 3 千字节的选择。它可以用作前端渲染的极简框架。后端的一个例子是来自第一章的Hapi.js项目。它专注于构建 API 端点的功能;如果你查看框架的源代码,你会在其核心部分找到少于 20 个文件。
重要的是要记住,你可以根据你的需求构建极简框架,并不总是需要复杂的工具和庞大的功能集。这种类型的方法并不仅仅关乎最终框架的大小或项目中的文件数量,它也可以作为你在框架开发过程中做决策时的指导原则。在许多方面,当这些类型的项目用于实际项目时,它们同样能够取得很好的成果。
构建工具
在第三章中,“框架编译器和打包器”部分展示了构建工具的示例。这些工具通常与框架一起提供,以实现优化后的应用程序包输出。一些框架还利用不同的构建工具类型或提供灵活的选项,允许利益相关者选择他们的构建工具。如今,构建工具的趋势是更容易为许多 JavaScript 环境生成输出,重点是速度。框架和构建工具结合的另一个主要方面是许多开发者合作改进构建工具工作流程,以适应不同项目需求的使用案例。
在流行的工具,如 webpack 中发现的构建工具的额外好处是强制执行良好的模式,并在应用程序输出不适合客户端或服务器环境时警告开发者。例如,打包工具可以在打包的包太大而无法由浏览器加载或可能不符合其目标客户端环境时警告开发者。
构建工具驱动着框架的持续优化,通过优化和进步,并为 JavaScript 应用程序的性能提升做出贡献,这是我们接下来要强调的下一个常见主题。
性能提升
对持续性能改进和基准测试的重视是 JavaScript 框架中相当典型的另一个主题。根据框架环境的不同,优化重点在于消耗更少的计算机资源、提高加载或响应时间、促进更流畅的用户交互、扩展渲染能力等。这类优化需要更深入地了解 JavaScript 语言以及优化现有代码的能力。对于更大的项目,优化过程也会变得越来越复杂。
自从早期 JavaScript 框架以来,已经建立了很多基准测试和基准测试工具。然而,在许多情况下,这类基准测试并不能真正基准测试真实用户行为或真实世界的应用程序使用案例。就像其他类型的软件基准测试一样,这些性能测试系统建立了一个标准测试来比较不同场景中的实现。尽管在许多情况下结果可能存在缺陷,但它们可以为试图找到适合其使用案例的框架的利益相关者提供一些见解。此外,即使经过多年的基准测试竞争,框架仍然希望展示它们在某些功能集上相对于竞争对手的优势。

图 10.3:一个开源的 js-framework-benchmark
图 10**.3 预览了 js-framework-benchmark 的过滤结果示例,这是一个专注于常见列表操作的基准测试。它可以在 krausest.github.io/js-framework-benchmark 找到。这个特定的基准测试专注于前端解决方案。如果你开发了一个前端框架,你可以将你的代码库添加到现有框架的列表中,以查看其比较情况。这类测试可以展示框架在包含复杂数据列表的应用程序中的表现,包括对数据行更新的处理。此基准测试还深入考虑了内存使用、加载时间和框架大小。
其他类型的框架需要不同类型的基准测试。例如,在 techempower.com/benchmarks 上有一个针对后端 Web 框架的全面比较。这些测试概述了针对服务器端框架的特定负载测试,并包括来自其他编程语言的项目。这类框架更关注请求的延迟、吞吐量、与数据库查询的交互等。框架的某些方面,如请求的吞吐量,展示了在特定框架下运行的服务器在重负载下的行为。
不同类型的框架可以利用它们框架空间中存在的类似测试。如果你的框架类型没有现有的测试类型,那么你可以建立自己的基准测试,并跟踪从发布到发布的成果,专注于提高与最相关功能集性能相关的数字。
今天框架的常见主题可以极大地影响你的框架项目,但要创造你构建的系统中的创新,还有更多需要从即将到来的趋势中学习。在下一节中,我们将探讨影响新 JavaScript 项目的未来趋势类型。
框架的未来
JavaScript 生态系统仍然是其他编程语言中最活跃的之一,并且注定会增长并扩展到新的领域。随着这种情况的发生,我们将开始看到影响框架构建和使用的未来创新。今天我们看到的开发者使用的模式也将随着时间的推移而改变。跟上行业趋势并关注未来将帮助你构建更好的系统,并将最新趋势融入你所构建的内容中。在本部分,我们将探讨一些框架演化和改进可能走向的潜在领域。
开发者体验
我们已经看到,在框架中,开发者体验(DX)可以有多么重要。在未来,DX 因素将进一步区分优秀的框架和伟大的框架。提供额外的工具和找到减少复杂性的方法,将使使用框架开始构建变得更加容易。在近年来,我们已经看到了实现端到端框架 DX 的策略,帮助利益相关者在构建应用程序的每个步骤中。出色的 DX 概念与减少使用框架的整体复杂性的概念非常契合。以下是一些未来 DX 改进将关注的主题:
-
降低学习曲线:框架作者将继续投资于使接口更易于接近,特别是对于新开发者。在服务器和前端环境中,这可能意味着进一步与 Web API 的结构保持一致,这有助于避免引入具有独特接口的新类型抽象。给未来框架开发者的一个建议是思考如何简化您项目的学习曲线。
-
简化配置:框架将进一步简化开箱即用的配置,选择最合适的选项将有助于开发。这包括更多合理的默认设置,正如我们在第七章中看到的,其中框架的某些部分专注于更简单的配置。对于您自己的项目,您应该注意您引入的每个配置选项,并避免让用户在必要的配置步骤中感到不知所措。
-
改进测试:项目将继续使编写和执行测试变得更加容易,进一步专注于简化难以测试的组件的测试。例如,随着新的端到端测试框架的开发,这些项目已经解决了测试中的常见开发者烦恼,如测试的不可靠性和 CI 环境中缺乏调试工具。在您的框架中强调简化测试可以帮助用户的日常体验,因为编写测试是一个耗时的工作。
-
package.json源代码。未来的项目可能会抽象出他们在后台使用的某些工具,从而在 DX 上提供更多控制。此外,项目还将构建更多自己的工具,使用生态系统中现有的原语。 -
关注性能:与常见主题部分中描述的性能改进类似,项目将继续推动 JavaScript 语言和运行时的极限,寻找在渲染、延迟和其他相关指标方面进行改进的方法。随着前端领域对性能的重视,开发者会遵循可以提高 Web 应用程序体验的质量指标。这些指标和概念可以在web.dev/learn-core-web-vitals找到。
-
额外的灵活性:框架将继续添加更多选项以支持不同的环境。例如,后端解决方案将扩大与更多数据库的集成。应用程序框架,如 Electron,将利用浏览器运行时和操作系统内可用的最新 API。对于你构建的框架,你应该在灵活性和支持的使用案例数量之间找到一个平衡点。只有在你有足够的资源来维护这个大型功能集时,你才应该为使用案例添加额外的支持。
-
打包和捆绑改进:框架打包应用程序代码的方式已经发生了显著变化。这种对改进打包技术的关注将继续,重点是速度和提供更多捆绑代码的不同方式。对于像你这样的框架开发者来说,跟上这些打包工具的最新改进非常重要,因为它们可以改善你构建系统打包的方法。
这个列表并没有涵盖 JavaScript 系统未来可能走向的所有潜在方向。然而,遵循现有项目的既定轨迹有助于突出你作为框架开发者可以提供重大影响并为你用户做出贡献的地方。
拥抱新技术
除了开发者体验的改进,新技术将在 JavaScript 应用程序执行的地方提供。正如在第一章中强调的,Web Assembly 将在为计算密集型任务提供下一代解决方案中发挥重要作用。这有可能将现有框架的代码库扩展到包括底层语言。这也将要求维护者将他们的领域知识扩展到仅限于 JavaScript 代码之外。在关于其他语言的议题上,TypeScript 的使用将继续增长,因为它的好处对框架作者来说非常值得。然而,框架的消费者仍然可以使用最初从 TypeScript 转换而来的构建版本的框架。
浏览器引擎的额外改进也将推动性能提升和在框架依赖浏览器引擎行为的地方带来新的功能。一个获取灵感的来源是groups.google.com/a/chromium.org/g/blink-dev上的blink-dev邮件列表,它突出了最终将出现在 Chromium 和 Chrome 浏览器中的即将到来的变化。
包管理也是将发生许多新技术变化的一个领域。几乎每个项目都依赖于包管理工具,如npm来解析和构建其代码库。包管理器在开发工作流程中扮演着如此重要的角色,任何改进都将对 JavaScript 开发产生重大影响。未来将带来更好的版本控制、依赖关系解析、项目增强安全性等。包管理器的演变也应当使框架组织和开发变得更加容易,允许作者以更符合开发者友好的方式布置他们的项目。
另一个重大新工具是利用 AI 驱动的模型来造福框架的各个方面,这在下一节中进行了探讨。
大型语言模型的使用
近年来,由不同神经网络驱动的大型语言模型(LLMs)的进步已经影响了软件工作流程和工具。作为框架开发的一个步骤,使用人工智能工具可能会变得更加普遍。类似于包含项目发布文档的重要性,开发者可能会捆绑一个专门为他们的框架训练的模型。该模型可以了解项目的公共 API、内部结构和文档。这些相互关联的模型可以在开发过程的各个阶段发挥作用。例如,当你重构代码时,由训练模型驱动的 AI 助手可以为你提供潜在更改的建议。随着这些模型集成越来越精细,它们也可能接管项目维护中的一些更重复或冗余的任务。

图 10.4:Astro AI 框架助手的示例
使用一个现实世界的例子,如图图 10**.4所示,一站式框架Astro专门训练了一个模型来回答关于项目的疑问。这个开发者工具可供公众使用,通过允许用户就项目提出问题来解决潜在的支持和集成障碍。该工具可在houston.astro.build上找到。这类工具可以接受关于项目某些功能的查询,例如创建 API 路由和端点。响应由来自文档和对话模型的混合文本组成。最终资源链接将用户引导至docs.astro.build/en/core-concepts/endpoints,其中包含有关为使用此框架的应用程序配置端点的信息。
结合使用训练有素的 AI 模型以改善开发者体验的类似想法,训练有素的 LLM 可以用于脚手架目的,并在框架工具本身中使用。在第八章中,我们看到了在 Componium 框架中生成代码的例子;一个潜在的未来增强功能可以由一个应用程序开发者关于他们试图构建的组件的查询来触发。后来生成的代码可以为开发者节省大量时间,减少他们需要阅读的文档量以及他们需要编写的代码量。创建和理解这些由 LLM 驱动的集成将是全栈开发者范式的一个额外步骤。在框架开发和维护的不同阶段集成这些类型的 LLM 将改变框架作者在项目中的大部分时间花费在哪里。
在涵盖了当前和未来的主题之后,让我们探讨在新的框架项目开发中我们还应该考虑哪些其他方面。
其他考虑因素
在第五章中,我们探讨了广泛的框架考虑因素,特别是影响技术架构的因素。由于创建一个新的框架项目,即使拥有完全独特的技术特性集,也是一个重大的决定,让我们再考察一些额外的考虑因素。除了技术挑战之外,这次考察将帮助我们决定从头开始启动一个项目并长期维护是否值得投资。成功推出一个拥有众多利益相关者和目标用户的框架项目将需要时间和财务支持。
除了技术障碍和架构挑战之外,框架开发者需要在开始新项目时考虑可能的投资回报。看看 JavaScript 空间中的一些成熟项目,其中许多都是多年的投资,有大量的开发者贡献了付费或开源时间以实现功能集。让我们在以下部分重点关注时间投资、用户支持和财务支持这三个特定的支柱。
时间投资
框架开发者需要考虑新项目的时间投资和生命周期。这不仅仅是关于开发阶段,还包括它所涉及的整个生命周期承诺,这可能影响组织内的多个团队或公司的更大目标。开源开发者也有类似的承诺需要考虑,但责任较小,尤其是在项目的早期阶段。与典型的 JavaScript 应用程序项目不同,框架开发通常没有线性时间表或指定的结束里程碑,届时可交付成果被视为完成。相反,这个过程是迭代的,包括开发周期、测试、改进和更新。这些主题与维护章节和新功能开发生命周期章节相联系。
一个框架项目将需要比一个开发者或整个团队创建的初始大量编码投资更多。为了适应不断变化的需求和技术规范,必要的持续更新将需要同等重要的承诺。考虑到我们在第九章中看到的任务,维护时间投资包括需要一个全面的流程,涉及许多开发者。当选择构建新的解决方案时,考虑你愿意投入多少时间以及你能成功执行多大规模的框架。做出深思熟虑的决定将决定你的框架努力是否成功。
用户支持
用户支持与时间投资一样重要,它往往决定了项目的成功。除了生产可读性和组织良好的文档外,对用户挑战和需求的理解也超越了代码库。对于所有类型的框架项目,作为开发者,你将发现自己或你的团队在充当支持和故障排除框架集成方面。以一个公开的例子来说,Vue.js 项目有一个讨论论坛,用户在那里发布他们的技术和架构问题:github.com/vuejs/core/discussions。团队有责任回答这些查询并保持良好的社区姿态。在公司内部的项目中,框架作者通常建立一个类似的平台与消费者互动。对于较小的公司,这种支持互动更加直接和实际。然而,在这些所有场景中,支持方面都占据了重要的开发时间。
减少这种类型流失的最佳策略是不断努力改进支持工作流程。改进措施包括整理常见问题以备后用,为复杂的框架集成创建指南,并尽可能使框架知识库易于发现。就像在第九章中看到的循环开发过程一样,作为维护者,你将发现自己对支持新用户和旧用户持续承诺。为了使项目成功,你必须把用户放在首位,并能以可接受的方式满足他们的需求。
所有这些在框架构建额外方面的投资都需要一个货币预算,我们将在下一节中重点介绍。
财务支持
在支持项目的复杂技术挑战、时间和物流之外,框架还需要考虑另一个因素——那就是财务支持的存在。资助一个新项目将需要承担特征开发和基础设施成本的不断支出。根据你的项目环境,可能会产生额外的成本——例如,如果你正在尝试推广企业框架给潜在客户。在公司环境中,如果框架间接支持基础服务和产品,资金可以从产品的利润中逐渐流入。
大多数开源项目通常没有完全的资金支持,但可能由一个拥有足够项目预算的大组织赞助,这也有助于间接推动开发工作或推进组织的议程。例如,Cypress 开源测试框架有一个付费的 Cypress Cloud 服务,它提供了一个专用环境。相反,一些开源开发者利用 Patreon 或 GitHub Sponsors 等平台来资助项目,直接从公司和个人用户那里获得财务贡献。
这些额外的考虑因素在规划和执行你的项目中可以发挥重要作用。根据你的开发目标,这些因素中的一些可能会使你试图建立的框架项目成功或失败。
摘要
本章提出了三个最终话题,这些话题可以帮助框架开发者更好地理解创建新框架的努力,这些话题都围绕着 JavaScript 开发的常见主题。对项目未来的展望也有助于开发者理解和准备在这个特定技术空间中即将到来的是什么。通过探索这些共同主题,你将了解模块化项目可以是什么样子,以及随着时间的推移,它们的设计是如何演变的,这使得做出关于自创项目的决策变得更加容易。同样,了解性能优化方法和独特的架构模式对潜在的框架作者也有益。
在第二部分中,本章强调了即将到来的趋势,并探讨了框架的未来潜力,为我们展示了这个空间即将到来的是什么。新一代的框架作者和维护者将体验到 JavaScript 环境的新挑战,并重构现有解决方案,使它们以新的方式更加能够应对。对更好的开发者体验和 LLMs 仍未知可能性的期望,使得框架空间对经验丰富的和新兴的 JavaScript 开发者来说都异常激动人心。
在结尾部分,本章强调了可以帮助开发者做出正确决策的额外考虑因素,这些非技术性考虑因素对开发者来说可能不那么吸引人,但它们对于框架创建过程至关重要。最后一章旨在提供更多见解,以增强你对框架开发的了解,并为你现实世界中的项目成功做好准备。
总之,本书及其所有章节的目标都是揭示框架开发方法,使构建自己的 JavaScript 框架的过程对所有开发者来说都更加易于接近。通过获得这些知识,你现在拥有了做出更好决策和推动更复杂的框架相关项目成功的技能。我们从对现有项目的旅程开始,确定了框架开发的关键部分,然后从零开始构建了一个全栈框架,这包括对架构模式和项目考虑因素的探索。
让我们总结一下在这些章节中学到的主题:
-
JavaScript 空间中其他项目的知识:第一章展示了不同类型的框架是如何出现以及它们为更广泛的开发者用户群体解决了哪些问题。这些项目的集合让我们窥见了现有的解决方案,这些解决方案有助于解决软件挑战。
-
框架组织和构建模块:这部分内容让我们了解了框架中抽象的基本原理,以及应用于后端、前端和其他类型项目的基本构建模块。
-
架构和模式:这部分展示了 Angular 和 Vue.js 等现有项目背后的概念和结构示例。这还包括了提及帮助将框架组合起来以供消费的额外工具。
-
确保框架质量:这部分内容让我们了解了框架如何通过创建文档、确保经过充分测试的组件以及如何通过开发工具帮助确保向用户交付优质软件来提供优质体验。
-
项目考虑因素的概述:这有助于制定开发计划,包括在开始解决技术问题之前需要考虑的各个方面。
-
创建新的测试框架:这为我们提供了创建实用框架项目的实践经验。它介绍了测试系统背后的技术架构,并具有详细的功能。
-
开发后端组件:这部分继续了实用方法,重点关注后端开发。我们概述了服务器端解决方案和 Componium 的开发者体验的方法。
-
构建前端组件:这是专注于前端组件的实用方法的最后一部分。它包括架构设计,涉及反应性、服务器端渲染等概念。
-
维护项目所需的努力:这部分内容让我们了解了框架开发者必须每天和周期性地执行的任务,随着新功能和修复的添加到项目中。
-
了解当今和未来的常见主题:最后,这部分总结了我们在 JavaScript 项目中今天看到的典型事物,并考虑了新项目和未来的额外因素。
所有这些新获得的知识将在 JavaScript 生态系统中为你解锁更多可能性,使你成为一个更有效且熟悉的工程师,极大地促进你的职业生涯和负责的项目。Web 应用程序开发领域在许多方面不断演变,这推动了 JavaScript 框架的激动人心的生态系统。即使有灵活的构建模块和成熟的解决方案,仍有进一步扩展和改进的空间——现在从未有过更好的时机来开发 JavaScript 框架或为其做出贡献!


浙公网安备 33010602011771号