框架主要功能
每个主要的 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}
周围的 curly braces(花括号)告诉应用程序读取 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
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
设置为其当前值加一。
组件样式
每个框架都提供了一种为组件或整个应用程序定义样式的方法。尽管每个框架定义组件样式的方法略有不同,但它们都提供了多种方式来完成此操作。通过添加一些辅助模块,你可以使用 Sass 或 Less 为你的框架应用程序设置样式,或者使用 PostCSS 转译你的 CSS 样式表。
处理依赖
所有主要框架都提供了处理依赖项的机制——在其他组件内部使用组件,有时具有多个层级。与其他特性一样,具体机制在不同框架之间会有所不同,但最终结果是相同的。组件通常使用标准的 JavaScript 模块语法,或至少类似的东西,将组件导入到其他组件中。
组件中的组件
基于组件的 UI 架构的一个关键好处是组件可以组合在一起。就像你可以在 HTML 标签内部编写 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 钻取,对于大型应用程序来说并不理想。
为了避免 props 钻取,框架提供了称为依赖注入的功能,这是一种将特定数据直接传递给需要它的组件,而无需经过中间层级的方法。每个框架以不同的名称和方式实现依赖注入,但效果最终是相同的。
Angular 将此过程称为依赖注入;Vue 具有provide()
和 inject()
组件方法;React 具有Context API;Ember 通过服务共享状态。
生命周期
在框架的语境中,组件的生命周期是一个组件从它被添加到 DOM 并由浏览器渲染(通常称为挂载)到它从 DOM 中移除(通常称为卸载)所经历的一系列阶段。每个框架对这些生命周期阶段的命名不同,并且并非所有框架都允许开发者访问相同的阶段。所有框架都遵循相同的通用模型:它们允许开发者在组件挂载时、渲染时、卸载时以及介于这些阶段之间的许多阶段执行某些操作。
渲染阶段是最关键的,因为它在用户与应用程序交互时重复的次数最多。每当浏览器需要渲染新的内容时,无论是浏览器中现有内容的添加、删除还是编辑,它都会运行。
此 React 组件生命周期图提供了对该概念的总体概述。
渲染元素
与生命周期一样,框架在如何渲染应用程序方面也采取了不同但相似的方法。所有框架都跟踪浏览器 DOM 的当前渲染版本,并且每个框架在应用程序中的组件重新渲染时对 DOM 如何改变做出略微不同的决策。由于框架为你做出这些决策,你通常不需要亲自与 DOM 交互。这种从 DOM 中抽象出来的行为比你自己更新 DOM 更复杂,也更占用内存,但没有它,框架就无法让你以它们闻名的声明性方式进行编程。
虚拟 DOM 是一种方法,其中有关浏览器 DOM 的信息存储在 JavaScript 内存中。你的应用程序更新此 DOM 副本,然后将其与“真实”DOM(即实际为用户渲染的 DOM)进行比较,以决定要渲染什么。应用程序构建一个“diff”来比较更新后的虚拟 DOM 和当前渲染的 DOM 之间的差异,并使用该 diff 将更新应用到真实 DOM。React 和 Vue 都使用虚拟 DOM 模型,但它们在 diffing 或渲染时并未应用完全相同的逻辑。
你可以在 React 文档中阅读更多关于虚拟 DOM 的信息。
增量 DOM 与虚拟 DOM 类似,因为它构建一个 DOM diff 来决定要渲染什么,但不同之处在于它不会在 JavaScript 内存中创建 DOM 的完整副本。它忽略了 DOM 中不需要更改的部分。Angular 是本模块中迄今为止讨论的唯一使用增量 DOM 的框架。
你可以在 Auth0 博客上阅读更多关于增量 DOM 的信息。
Glimmer VM 是 Ember 独有的。它既不是虚拟 DOM 也不是增量 DOM;它是一个独立的过程,通过它 Ember 的模板被转译成一种比 JavaScript 更容易和更快读取的“字节码”。
路由
正如上一章提到的,路由是 Web 体验的重要组成部分。为了避免在具有许多视图的足够复杂的应用程序中出现中断的体验,本模块中介绍的每个框架都提供了一个(或多个)库,帮助开发者在其应用程序中实现客户端路由。
测试
所有应用程序都受益于测试覆盖,这确保你的软件继续按预期运行,Web 应用程序也不例外。每个框架的生态系统都提供有助于编写测试的工具。测试工具不内置于框架本身,但用于生成框架应用程序的命令行界面工具使你能够访问适当的测试工具。
每个框架的生态系统都有广泛的工具,包括单元测试和集成测试功能。
Testing Library 是一套测试实用工具,为许多 JavaScript 环境提供工具,包括 React、Vue 和 Angular。Ember 文档涵盖了 Ember 应用程序的测试。
这是使用 React Testing Library 编写的 CounterButton
的一个快速测试——它测试了许多方面,例如按钮的存在,以及按钮在被点击 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");
});
总结
至此,你应该对使用框架创建应用程序时将实际使用的语言、特性和工具有了更深入的了解。我确信你对开始动手编写代码充满热情,而这正是你接下来要做的!