框架主要功能
每个主要的 JavaScript 框架都有不同的方法来更新 DOM,处理浏览器事件,并提供愉快的开发体验。本文将探讨“四大”框架的主要功能,从高层次了解框架的工作方式,以及它们之间的区别。
先决条件 | 熟悉核心 HTML、CSS 和 JavaScript 语言。 |
---|---|
目标 | 了解框架的主要代码功能。 |
领域特定语言
本模块中讨论的所有框架都由 JavaScript 提供支持,并且都允许您使用特定领域语言 (DSL) 来构建您的应用程序。特别是,React 推广了使用 **JSX** 来编写其组件,而 Ember 使用 **Handlebars**。与 HTML 不同,这些语言知道如何读取数据变量,并且这些数据可用于简化编写 UI 的过程。
Angular 应用程序通常大量使用 **TypeScript**。TypeScript 不关心用户界面的编写,但它是一种特定领域语言,并且与普通 JavaScript 存在显著差异。
DSL 不能被浏览器直接读取;它们必须先转换为 JavaScript 或 HTML。 转换是开发过程中的一个额外步骤,但框架工具通常包含处理此步骤所需的工具,或者可以调整以包含此步骤。虽然可以在不使用这些特定领域语言的情况下构建框架应用程序,但使用它们将简化您的开发过程,并使您更容易从这些框架周围的社区获得帮助。
JSX
JSX 代表 JavaScript 和 XML,是 JavaScript 的扩展,它将 HTML 类似语法引入 JavaScript 环境。它由 React 团队发明,用于 React 应用程序,但可用于开发其他应用程序,例如 Vue 应用程序。
以下是简单的 JSX 示例
const subject = "World";
const header = (
<header>
<h1>Hello, {subject}!</h1>
</header>
);
此表达式表示一个 HTML <header>
元素,其中包含一个 <h1>
元素。{subject}
周围的花括号告诉应用程序读取 subject
常量的值并将其插入我们的 <h1>
中。
与 React 一起使用时,上一段中的 JSX 将编译成以下内容
const subject = "World";
const header = React.createElement(
"header",
null,
React.createElement("h1", null, "Hello, ", subject, "!"),
);
最终由浏览器渲染时,上面的代码段将生成看起来像这样的 HTML
<header>
<h1>Hello, World!</h1>
</header>
Handlebars
The Handlebars 模板语言不特定于 Ember 应用程序,但在 Ember 应用程序中被大量使用。Handlebars 代码类似于 HTML,但可以选择从其他地方提取数据。此数据可用于影响应用程序最终构建的 HTML。
与 JSX 一样,Handlebars 使用花括号来插入变量的值。Handlebars 使用一对双花括号,而不是一对单花括号。
给出以下 Handlebars 模板
<header>
<h1>Hello, {{subject}}!</h1>
</header>
以及以下数据
{
subject: "World";
}
Handlebars 将构建这样的 HTML
<header>
<h1>Hello, World!</h1>
</header>
TypeScript
TypeScript 是 JavaScript 的超集,这意味着它扩展了 JavaScript — 所有 JavaScript 代码都是有效的 TypeScript,反之则不然。TypeScript 对于开发人员能够在其代码上实施的严格性很有用。例如,考虑一个函数 add()
,它接受整数 a
和 b
并返回它们的总和。
在 JavaScript 中,该函数可以这样编写
function add(a, b) {
return a + b;
}
这段代码对于习惯了 JavaScript 的人来说可能很简单,但它仍然可以更清晰。JavaScript 允许我们使用 +
运算符将字符串连接在一起,因此此函数在技术上仍然可以工作,即使 a
和 b
是字符串,它可能不会给你预期的结果。如果我们只想允许将数字传递给此函数怎么办?TypeScript 使这成为可能
function add(a: number, b: number) {
return a + b;
}
每个参数后面写的 : number
告诉 TypeScript a
和 b
都必须是数字。如果我们要使用此函数并将 '2'
作为参数传递给它,TypeScript 将在编译期间引发错误,我们必须修复我们的错误。我们可以编写我们自己的 JavaScript 来为我们引发这些错误,但这会使我们的源代码变得冗长。也许让 TypeScript 为我们处理此类检查更有意义。
编写组件
如上一章所述,大多数框架都具有一定的组件模型。React 组件可以使用 JSX 编写,Ember 组件可以使用 Handlebars 编写,Angular 和 Vue 组件可以使用稍微扩展了 HTML 的模板语法编写。
无论他们对如何编写组件有什么意见,每个框架的组件都提供了一种方法来描述它们可能需要的外部属性,组件应该管理的内部状态,以及用户可以在组件标记上触发的事件。
本节其余部分的代码片段将以 React 为例,并使用 JSX 编写。
属性
属性或 **props** 是组件为了渲染而需要的外部数据。假设您正在为在线杂志构建一个网站,并且您需要确保每位投稿作者都获得了他们作品的署名。您可能会为每篇文章创建一个 AuthorCredit
组件。此组件需要显示作者的肖像和关于他们的简短简介。为了知道要渲染哪个图像以及要打印哪个简介,AuthorCredit
需要接受一些 props。
AuthorCredit
组件的 React 表示可能看起来像这样
function AuthorCredit(props) {
return (
<figure>
<img src={props.src} alt={props.alt} />
<figcaption>{props.byline}</figcaption>
</figure>
);
}
{props.src}
、{props.alt}
和 {props.byline}
代表 props 将插入组件的位置。要渲染此组件,我们将在我们要渲染它的地方编写这样的代码(这很可能在另一个组件内部)
<AuthorCredit
src="./assets/zelda.png"
alt="Portrait of Zelda Schiff"
byline="Zelda Schiff is editor-in-chief of the Library Times."
/>
这将最终在浏览器中渲染以下 <figure>
元素,其结构如 AuthorCredit
组件中定义的那样,其内容如 AuthorCredit
组件调用中包含的 props 中定义的那样
<figure>
<img src="assets/zelda.png" alt="Portrait of Zelda Schiff" />
<figcaption>Zelda Schiff is editor-in-chief of the Library Times.</figcaption>
</figure>
状态
我们在上一章讨论过 **状态** 的概念 — 强大的状态处理机制是有效框架的关键,每个组件可能都有一些需要控制其状态的数据。只要组件在使用,此状态就会以某种方式持续存在。与 props 一样,状态可用于影响组件的渲染方式。
例如,考虑一个按钮,它计算它被点击了多少次。此组件应该负责跟踪它自己的计数状态,并且可以这样编写
function CounterButton() {
const [count] = useState(0);
return <button>Clicked {count} times</button>;
}
useState()
是一个 **React hook**,它在给定初始数据值的情况下,会跟踪该值在更新时的变化。代码将最初像这样在浏览器中渲染
<button>Clicked 0 times</button>
useState()
调用以一种稳健的方式在整个应用程序中跟踪 count
值,而无需您自己编写代码来执行此操作。
事件
为了具有交互性,组件需要能够响应浏览器事件,以便我们的应用程序能够响应我们的用户。每个框架都提供了自己的语法来监听浏览器事件,这些事件引用了等效的本机浏览器事件的名称。
在 React 中,监听 click
事件需要一个特殊的属性 onClick
。让我们更新上面的 CounterButton
代码,使其能够计算点击次数
function CounterButton() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
);
}
在此版本中,我们使用额外的 useState()
功能来创建一个特殊的 setCount()
函数,我们可以调用它来更新 count
的值。我们在 onClick
事件处理程序内调用此函数,将 count
设置为其当前值加上一。
样式化组件
处理依赖项
所有主要框架都提供了用于处理依赖项的机制 — 在其他组件内部使用组件,有时使用多个层次结构级别。与其他功能一样,确切的机制因框架而异,但最终结果相同。组件往往使用标准的 JavaScript 模块语法 将组件导入其他组件,或者至少使用类似的东西。
组件中的组件
基于组件的 UI 架构的一个关键优势是组件可以组合在一起。就像您可以将 HTML 标签彼此嵌套以构建网站一样,您也可以在其他组件内使用组件来构建 Web 应用程序。每个框架都允许您编写利用(因此依赖于)其他组件的组件。
例如,我们的 AuthorCredit
React 组件可能在 Article
组件内使用。这意味着 Article
需要导入 AuthorCredit
。
import AuthorCredit from "./components/AuthorCredit";
完成后,AuthorCredit
可以在 Article
组件内像这样使用
<Article>
<AuthorCredit />
</Article>
依赖注入
现实世界中的应用程序通常涉及具有多个嵌套级别的组件结构。AuthorCredit
组件在许多级别深度嵌套可能出于某种原因需要来自我们应用程序最顶层的根级别的数据。
假设我们正在构建的杂志网站结构如下
<App>
<Home>
<Article>
<AuthorCredit {/* props */} />
</Article>
</Home>
</App>
我们的 App
组件具有一些我们的 AuthorCredit
组件需要的数据。我们可以重写 Home
和 Article
,以便它们知道如何将 props 传递下去,但这如果我们的数据起源和目的地之间存在很多级别,可能会变得很繁琐。这也是多余的:Home
和 Article
实际上并不使用作者的肖像或简介,但如果我们想将该信息放入 AuthorCredit
,我们将需要更改 Home
和 Article
来适应它。
通过多层组件传递数据的这个问题称为 props 钻取,对于大型应用程序来说并不理想。
为了避免属性传递(prop drilling),框架提供了依赖注入的功能,这是一种将特定数据直接传递给需要它的组件的方法,无需通过中间层传递。每个框架都以不同的名称和方式实现依赖注入,但最终的效果是一样的。
Angular 将此过程称为 依赖注入;Vue 有 provide()
和 inject()
组件方法;React 有 Context API;Ember 通过 服务 共享状态。
生命周期
在框架的上下文中,组件的 **生命周期** 是组件从被附加到 DOM 并由浏览器渲染(通常称为 *挂载*)到从 DOM 中删除(通常称为 *卸载*)所经历的一系列阶段。每个框架对这些生命周期阶段的命名不同,并非所有框架都允许开发人员访问相同的阶段。所有框架都遵循相同的通用模型:它们允许开发人员在组件 *挂载*、*渲染*、*卸载* 以及这些阶段之间进行许多操作时执行特定操作。
渲染 阶段是最重要的,因为当用户与应用程序交互时,它会被重复执行最多次。每当浏览器需要渲染新的内容时,它都会运行,无论新信息是添加到浏览器中的内容、删除的内容还是对现有内容的编辑。
这个 React 组件生命周期的图表 提供了对该概念的概览。
渲染元素
与生命周期一样,框架对如何渲染应用程序采用了不同但类似的方法。它们都跟踪浏览器 DOM 的当前渲染版本,并根据组件在应用程序中重新渲染的方式做出略微不同的决定来改变 DOM。由于框架为您做出了这些决定,因此您通常不会自己与 DOM 交互。这种对 DOM 的抽象比自己更新 DOM 更复杂,也更消耗内存,但没有它,框架将无法让您以它们所知的方式进行声明式编程。
**虚拟 DOM** 是一种方法,通过这种方法,关于浏览器 DOM 的信息存储在 JavaScript 内存中。应用程序更新此 DOM 副本,然后将其与“真实” DOM(为用户实际渲染的 DOM)进行比较,以决定要渲染的内容。应用程序构建一个“差异”来比较更新后的虚拟 DOM 与当前渲染的 DOM 之间的差异,并使用该差异将更新应用于真实 DOM。React 和 Vue 都使用虚拟 DOM 模型,但它们在进行差异比较或渲染时不使用完全相同的逻辑。
您可以 在 React 文档中阅读有关虚拟 DOM 的更多信息。
**增量 DOM** 与虚拟 DOM 类似,它构建了一个 DOM 差异来决定要渲染的内容,但不同之处在于它没有在 JavaScript 内存中创建完整的 DOM 副本。它忽略了不需要更改的 DOM 部分。Angular 是本模块中迄今为止讨论过的唯一使用增量 DOM 的框架。
您可以 在 Auth0 博客中阅读有关增量 DOM 的更多信息。
**Glimmer VM** 是 Ember 独有的。它不是虚拟 DOM 也不是增量 DOM;它是一个独立的过程,通过它,Ember 的模板被转译成一种类似于“字节码”的东西,这种字节码比 JavaScript 更易于读取和更快速。
路由
如 前一章所述,路由 是 Web 体验的重要组成部分。为了避免在具有大量视图的足够复杂的应用程序中出现故障体验,本模块中介绍的每个框架都提供了一个库(或多个库)来帮助开发人员在其应用程序中实现客户端路由。
测试
所有应用程序都受益于测试覆盖范围,这确保了您的软件能够继续按预期的方式运行,Web 应用程序也不例外。每个框架的生态系统都提供了有助于编写测试的工具。测试工具本身并非内置在框架中,但用于生成框架应用程序的命令行界面工具允许您访问适当的测试工具。
每个框架在其生态系统中都有广泛的工具,具有执行单元测试和集成测试的功能。
Testing Library 是一套测试实用程序,它包含许多 JavaScript 环境(包括 React、Vue 和 Angular)的工具。Ember 文档涵盖了 Ember 应用程序的测试。
以下是对我们的 CounterButton
的快速测试,它是借助 React Testing Library 编写的 - 它测试了多方面内容,例如按钮的存在,以及在被点击 0 次、1 次和 2 次后按钮是否显示了正确的文本。
import { fireEvent, render, screen } from "@testing-library/react";
import CounterButton from "./CounterButton";
it("Renders a semantic button with an initial state of 0", () => {
render(<CounterButton />);
const btn = screen.getByRole("button");
expect(btn).toBeInTheDocument();
expect(btn).toHaveTextContent("Clicked 0 times");
});
it("Increments the count when clicked", () => {
render(<CounterButton />);
const btn = screen.getByRole("button");
fireEvent.click(btn);
expect(btn).toHaveTextContent("Clicked 1 times");
fireEvent.click(btn);
expect(btn).toHaveTextContent("Clicked 2 times");
});