框架的主要特性

378 阅读13分钟

翻译自 developer.mozilla.org/en-US/docs/…

每种Javascript框架都有自己不同的方式更新DOM,处理浏览器事件,为开发者提供愉快的使用体验,这篇文章将探索“四大”框架的主要特性,从高级角度探讨框架的工作方式以及它们之间的区别。

特定领域语言(DSL)

在本文中讨论的所有框架都是javascript驱动的,允许你使用DSL来建立自己的应用。尤其是,React大规模使用JSX来写它自己的组件,Ember使用了Handlebars。不像HTML,这些语言知道怎样读取数据值,这些值能够用来简化编写UI的流程。

Angular应用经常大量使用Typescript。Typesscrit并不涉及UI的编写,但它是一种DSL语言,与Javascript相比有着一些值得注意的区别。

DSL并不能被浏览器直接运行;它们必须先被转换成Javascript或者HTML。在开发的过程中,转换是一种额外的步骤,但是框架模板普遍都会包含必须的工具去处理这个步骤,或者能够被配置来包含这个步骤。在建立框架应用的时候可以不使用这些DSL语言,但拥抱它们将会简化你的开发流程,并且更容易在这些框架社区寻求帮助。

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>元素。第4行中被花括号包起来的subject告诉应用去读取subject的值,然后把它插入到我们的<h1>

当使用React的时候,来自于前一个片段的JSX就会被编译成这个样子:

var subject = "World";
var 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 使用双花括号,而不是单花括号。

给个模板的例子

<header>
  <h1>Hello, {{subject}}!</h1>
</header>

和这个数据

{
  subject: "World"
}

HandleBars将会构建出像这样的页面

<header>
  <h1>Hello, World!</h1>
</header>

TypeScript

TypeScript 是 JavaScript 的超集,这意味着它扩展了 JavaScript——所有 JavaScript 代码都是有效的 TypeScript,但反之则不然。 TypeScript 非常有用,因为它允许开发人员强制执行他们的代码。 例如,考虑一个函数 add(),它接受整数 a 和 b 并返回它们的总和。

在Js中,这个方法长这样

function add(a, b) {
  return a + b;
}

对于习惯使用 JavaScript 的人来说,这段代码可能是微不足道的,但它仍然可以更清晰。 JavaScript 允许我们使用 + 运算符将字符串连接在一起,所以如果 a 和 b 是字符串,这个函数在技术上仍然可以工作——它可能不会给你期望的结果。 如果我们只想让数字传入这个函数怎么办? TypeScript 使这成为可能:

function add(a: number, b: number) {
  return a + b;
}

这里每个参数后面的 : 数字告诉 TypeScript a 和 b 都必须是数字。 如果我们使用这个函数并将 '2' 作为参数传递给它,TypeScript 会在编译期间引发错误,我们将被迫修复我们的错误。 我们可以编写自己的 JavaScript 来为我们引发这些错误,但这会使我们的源代码变得更加冗长。 让 TypeScript 为我们处理此类检查可能更有意义。(注:其实从我用ts到现在,ts的类型检查并不是例子里这么简单,想真正的用明白ts真的很有难度,我想用ts开发过大型项目的小伙伴们应该深有体会吧)。

Writing Components(写组件)

如前一章所述,大多数框架都有某种组件模型。 React 组件可以使用 JSX 编写,Ember 组件使用 Handlebars 编写,Angular 和 Vue 组件可以使用模板语法轻松扩展 HTML。 不管他们对如何编写组件有任何看法,每个框架的组件都提供了一种方法来描述它们可能需要的外部属性、组件应该管理的内部状态以及用户可以在组件标记上触发的事件。 本节其余部分的代码片段将使用 React 作为示例,并使用 JSX 编写。

Properties(属性)

这个 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."
/>

这将最终在浏览器中呈现以下

元素,其结构在 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>

State(状态)

我们在上一章中谈到了状态的概念——一个健壮的状态处理机制是有效框架的关键,每个组件可能都有需要其状态控制的数据。 只要组件在使用中,这种状态就会以某种方式持续存在。 与 props 一样,state 可用于影响组件的呈现方式。 

举个例子,考虑一个按钮,它计算它被点击了多少次。 这个组件应该负责跟踪它自己的计数状态,可以这样写:

function CounterButton() {
  const [count] = useState(0);
  return (
    <button>Clicked {count} times</button>
  );
}

useState() 是一个 React 钩子,给定一个初始数据值,它会在更新时跟踪该值。 代码最初将在浏览器中呈现如下:

<button>Clicked 0 times</button>

useState() 调用在整个应用程序中以稳健的方式跟踪计数值,而无需您自己编写代码来执行此操作。

Events(事件)

为了具有交互性,组件需要响应浏览器事件的方法,以便我们的应用程序可以响应我们的用户。 每个框架都提供了自己的语法来侦听浏览器事件,这些语法引用了等效的本机浏览器事件的名称。 在 React 中,监听点击事件需要一个特殊的属性 onClick。 

让我们从上面更新我们的 CounterButton 代码以允许它计算点击次数:

function CounterButton() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
  );
}

在这个版本中,我们使用额外的 useState() 功能来创建一个特殊的 setCount() 函数,我们可以调用它来更新 count 的值。 我们在第 4 行调用这个函数,并将 count 设置为其当前值加一。

Styling Components(样式组件)

每个框架都提供了一种为您的组件或整个应用程序定义样式的方法。 尽管每个框架定义组件样式的方法略有不同,但它们都为您提供了多种方法。 通过添加一些辅助模块,您可以在 Sass 或 Less 中设置框架应用程序的样式,或者使用 PostCSS 转换您的 CSS 样式表。

Handling dependencies(处理依赖)

所有主要框架都提供了处理依赖关系的机制——在其他组件中使用组件,有时具有多个层次结构。 与其他功能一样,框架之间的确切机制会有所不同,但最终结果是相同的。 组件倾向于使用标准的 JavaScript 模块语法或至少类似的东西将组件导入到其他组件中。

Components in components(组件中的组件)

基于组件的 UI 架构的一个主要好处是组件可以组合在一起。 就像您可以在彼此内部编写 HTML 标签来构建网站一样,您可以使用其他组件内部的组件来构建 Web 应用程序。 每个框架都允许您编写利用(并因此依赖)其他组件的组件。

例如,我们的 AuthorCredit React 组件可能会在文章组件中使用。 这意味着文章需要导入 AuthorCredit。

import AuthorCredit from "./components/AuthorCredit";

Once that’s done, AuthorCredit could be used inside the Article component like this:

  ...

<AuthorCredit />

  ...

Dependency injection(依赖注入)

实际应用程序通常会涉及具有多层嵌套的组件结构。 一个 AuthorCredit 组件嵌套了很多层,出于某种原因,它可能需要来自我们应用程序最根级别的数据。 

假设我们正在构建的杂志网站的结构如下:

<App>
  <Home>
    <Article>
      <AuthorCredit {/* props */} />
    </Article>
  </Home>
</App>

我们的 App 组件有我们的 AuthorCredit 组件需要的数据。我们可以重写 Home 和 Article,以便它们知道将 props 传递下去,但是如果我们的数据的来源和目的地之间有很多很多层次,这可能会变得乏味。这也太过分了:首页和文章实际上并没有使用作者的肖像或署名,但是如果我们想将这些信息放入 AuthorCredit 中,我们需要更改 Home 和 Author 以适应它。 

通过多层组件传递数据的问题称为道具钻取,对于大型应用程序来说并不理想。

为了规避 prop 钻取,框架提供了称为依赖注入的功能,这是一种将某些数据直接获取到需要它的组件的方法,而无需通过中间级别。每个框架都以不同的名称、不同的方式实现依赖注入,但最终效果是一样的。

Angular 称这个过程为依赖注入; Vue 有 provide() 和 inject() 组件方法; React 有一个 Context API; Ember 通过服务共享状态。(这里划重点)

Lifecycle(生命周期)

在框架的上下文中,组件的生命周期是组件从浏览器呈现(通常称为挂载)到从 DOM 中删除(通常称为卸载)所经历的阶段的集合。 每个框架以不同的方式命名这些生命周期阶段,并且并非所有框架都允许开发人员访问相同的阶段。 所有框架都遵循相同的通用模型:它们允许开发人员在组件安装、渲染、卸载以及这些之间的许多阶段执行某些操作。 

渲染阶段是最需要理解的,因为当您的用户与您的应用程序交互时,它重复的次数最多。 每次浏览器需要呈现新信息时,它都会运行,无论新信息是浏览器中内容的添加、删除还是编辑。

这张 React 组件生命周期图提供了该概念的一般概述。

Rendering elements(渲染元素)

就像生命周期一样,框架采用不同但相似的方法来呈现您的应用程序。它们都跟踪浏览器 DOM 的当前呈现版本,并且每个都对 DOM 应该如何随着应用程序中的组件重新呈现而发生变化做出略微不同的决定。由于框架会为您做出这些决定,因此您通常不会自己与 DOM 进行交互。这种对 DOM 的抽象比自己更新 DOM 更复杂、更占用内存,但如果没有它,框架将无法允许您以它们众所周知的声明式方式进行编程。

虚拟 DOM 是一种将有关浏览器 DOM 的信息存储在 JavaScript 内存中的方法。您的应用程序更新此 DOM 副本,然后将其与“真实”DOM(实际为用户呈现的 DOM)进行比较,以确定要呈现的内容。应用程序构建一个“差异”来比较更新的虚拟 DOM 和当前呈现的 DOM 之间的差异,并使用该差异将更新应用于真实 DOM。 React 和 Vue 都使用虚拟 DOM 模型,但它们在差异或渲染时不应用完全相同的逻辑。 

您可以在 React 文档中阅读有关 Virtual DOM 的更多信息。 

增量 DOM 类似于虚拟 DOM,因为它构建了一个 DOM 差异来决定渲染什么,但不同之处在于它不会在 JavaScript 内存中创建 DOM 的完整副本。它忽略不需要更改的 DOM 部分。 Angular 是本模块迄今为止讨论的唯一使用增量 DOM 的框架。 您可以在 Auth0 博客上阅读有关增量 DOM 的更多信息。 Glimmer VM 是 Ember 独有的。它既不是虚拟 DOM,也不是增量 DOM;它是一个单独的过程,通过这个过程,Ember 的模板被转换成一种比 JavaScript 更容易、更快阅读的“字节码”。(Ember这个有意思了,国内好像不怎么使用它)

Routing(路由)

如前一章所述,路由是 Web 体验的重要组成部分。 为了避免在具有大量视图的足够复杂的应用程序中出现糟糕的体验,本模块中涵盖的每个框架都提供了一个库(或多个库),可帮助开发人员在其应用程序中实现客户端路由。

Testing(测试)

所有应用程序都受益于测试覆盖率,确保您的软件继续按照您期望的方式运行,Web 应用程序也不例外。 每个框架的生态系统都提供了有助于编写测试的工具。 测试工具并未内置于框架本身,但用于生成框架应用程序的命令行界面工具可让您访问适当的测试工具。 

每个框架在其生态系统中都有广泛的工具,具有单元和集成测试等功能。

测试库是一套测试实用程序,具有适用于许多 JavaScript 环境的工具,包括 React、Vue 和 Angular。 Ember 文档涵盖了 Ember 应用程序的测试。 

这是在 React 测试库的帮助下对我们的 CounterButton 进行的快速测试——它测试了很多东西,比如按钮的存在,以及按钮在被点击 0、1 和 2 次后是否显示正确的文本:

import React from "react";
import { render, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

import CounterButton from "./CounterButton";

it("renders a semantic with an initial state of 0", () => {
  const { getByRole } = render(<CounterButton />);
  const btn = getByRole("button");

  expect(btn).toBeInTheDocument();
  expect(btn).toHaveTextContent("Clicked 0 times");
});

it("Increments the count when clicked", () => {
  const { getByRole } = render(<CounterButton />);
  const btn = getByRole("button");

  fireEvent.click(btn);
  expect(btn).toHaveTextContent("Clicked 1 times");

  fireEvent.click(btn);
  expect(btn).toHaveTextContent("Clicked 2 times");
});

Summary(总结)

在这一点上,您应该对使用框架创建应用程序时将使用的实际语言、特性和工具有更多的了解。 我敢肯定,您热衷于开始并实际进行一些编码,这就是您接下来要做的事情! 此时,您可以选择要先开始学习的框架:

  • React

  • Amber

  • Vue

    注意:我们现在只有三个框架教程系列可用,但我们希望将来有更多可用的。