What will be learned

  • 命令式(imperative)编程和声明式(declarative)编程的区别
  • React 组件和实例,React 如何通过组件控制 UI 流
  • React 是如何改变我们的 web 构建方式,推行一个全新的概念:关注点分离(separation of concerns)
  • 为什么人们对 JavaScript 很疲倦,并且了解我们如何在开发中通过使用 React 来避免常见的 JavaScript 错误
  • TypeScript 如何改变这场闹剧

Differentiating between declarative and imperative programming

  • 命令式编程:描述工作步骤(how things work)
  • 声明式编程:描述你想达到的结果(whatyou want to achieve)

Java Script就是典型的命令式编程,例如,我们希望创建一个切换按钮,可以切换on/off,我们需要编写如下代码:

const toggleButton = document.querySelector('#toggle')
  toogleButton.addEventListener('click', () => {
    if (toggleButton.classList.contains('on') ) {
      toggleButton.classList.remove('on')
      toggleButton.classList.add('off')
    } else {
      toggleButton.classList.remove('off')
      toggleButton.classList.add('on')
    }
}

可以看到,我们详述了所有关于如何切换的步骤,而对于声明式编程 React,我们只需要编写如下代码:

// To turn on the Toggle
<Toggle on />
// To turn off the toggle
<Toggle off />

我们只需要编写我们希望达到的状态,on或者off,即可实现上述的功能。

如此简洁优雅的代码必然会使我们遇到更少的bug,并且有更高的可维护性。

How React elements work

虽然大多数读者应该对 React Components 耳熟能详,但如果想要更有效的使用 React,我们需要了解一个一个更基础的知识——React Element

为了控制 UI 流,React 使用了一个特定类型的对象,即 React Element,它描述了应该显示在屏幕上的内容是什么。Element 比 Component 更为简单,Element 是不可变的(immutable),它仅包含需要显示在屏幕上的直接内容。

一个常见的 element 实例:

{
    type: Title,
    props: {
      color: 'red',
      children: 'Hello, Title! '
  }
}

其中,type 是 element 最重要的属性,除此之外,element 还可以包含一些其他属性,其中就有一个特别的属性 children ,该属性是可选的,描述的是当前 element 实例的直接子元素(direct descendant of the element.)

Type 是 element,最重要的属性,因为该属性告诉 React 应该如何处理该 element,当 type 为字符串时,该 element 代表一个 DOM 节点,当 type 是一个函数时,该 element 代表一个 Component 组件。

代表 DOM 元素的 element 和代表React components 的 element 可以相互嵌套,代表一个渲染树,如下所示:

{
  type: Title, // 代表 React component
  props: {
    color: 'red',
    children: {
      type: 'h1', // 代表 DOM 元素
      props: {
        children: 'Hello, H1! '
      }
    }
  }
}

React通过以下规则解析该渲染树:当type是一个函数时,React调用该函数,传入 props 以获得低一级的元素(children),由于存在嵌套的可能,因此 React 会递归的重复上述执行模式,直到React获得的元素为可以直接渲染在屏幕的DOM元素,这种过程被称作和解(reconciliation)

Unlearning everything

在过去的二十年,我们的web设计遵循了关注点分离(separation of concerns)的原则,关注点分离,就是说将一个整体拆分(分离)成不同部分,并对于每一个部分进行研究(关注点),对于web 设计来说,即将一个web网页拆分成template, javascript, css三个部分,分别进行编写,如下面一个最常见的模板语言:

{{#items}}
  {{#first}}
    <li><strong>{{name}}</strong></li>
  {{/first}}
  {{#link}}
    <li><a href="{{url}}">{{name}}</a></li>
  {{/link}}
{{/items}}

上述代码为 Mustache system,是一种常见的模板语法,该代码中的items表示一个集合,内部的代码表示遍历这个集合,对每个元素进行的操作,first 表示对第一个元素的处理,link 则处理剩下元素,你不必了解该语法的详细内容,本例只是希望让你体会一件事:

  • 模板高度依赖于它们从逻辑层接收到的模型来显示信息。
  • 同样,样式也很有可能依赖从逻辑层接收到的模型来显示。

这说明,虽然我们可以将web页面分为html, css, js,但实际上它们之间是相互耦合(coupling)的,这虽然不是一件坏事,但缺没有真正的做到关注点分离。

Understanding JavaScript fatigue

有一种流行的观点认为 React 由大量的技术和工具组成,如果你想使用它,你必须与包管理器、转译器、模块绑定器以及无限多的不同库打交道。这个想法在人们中非常广泛,以至于它已经被明确定义,并被命名为JavaScript疲劳( JavaScript fatigue.)。

实际上 React 仅由两个包组成:

  • react: 实现了库的核心功能,如创建React element
  • react-dom: 包含所有与浏览器相关的特性,如把React创建出来的element,通过DOM API, 创建出真实 DOM 元素。

如果仅使用上述两个包,我们就不能使用 jsx 语法,因为这不是一个被浏览器支持的标准语言,但是我们能迅速使用最低程度的React功能。在初期,我们仅仅是使用了createElement这一功能,你可以随着需求逐渐引入其他功能。通过包含转译器来使用jsx/tsx语法。

React 的开发者 Facebook非常关心开发者体验,因此它们发布了 CLI 工具(Command Line Interface,命令行界面),供开发者快速构建和运行一个真正的React应用,它使用npm就可以安装:

npm install -g create-react-app
create-react-app hello-world --template typescript
cd hello-world
npm start

在你期望的路径打开cmd窗口并实行该命令,你就拥有一个具备了使用最先进的技术构建的完整React应用程序所需的所有特性。

covers/React 17 Design Patterns Best Practices

具有完整技术构建的React应用

Introducing TypeScript

TypeScript是JavaScript的类型化超集,它最终被编译成JavaScript,这意味着TypeScript是带有一些附加特性的JavaScript。我们将介绍一些TypeScript的重要特点。

TypeScript features

  • TypeScript is JavaScript: 我们编写的任何JavaScript代码都可以在TypeScript中执行,因此当你懂得JavaScript语法时,基本上你就可以开始使用TypeScript,你只需要如何向代码中添加类型约束。所有的TypeScript代码最后都会转换成JavaScript。
  • JavaScript is TypeScript: 这意味着你可以将所有的.js文件重命名为.ts文件,它仍然会正常工作。
  • Error checking: TypeScript会编译代码,检查错误并高亮显示,这会帮助你更好的运行代码。
  • Strong typing: 默认情况下,JavaScript并不是强类型的,当你使用TypeScript时,你就可以对变量和函数添加类型,你甚至可以指定返回值的类型。
  • Object-oriented programming supported: TypeScript支持类,继承,接口等面向对象的概念。

Converting JavaScript code into TypeScript

假设我们需要编写一个函数来判断一个单词是否回文,JavaScript代码应该如下所示:

function isPalindrome(word) {
  const lowerCaseWord = word.toLowerCase()
  const reversedWord = lowerCaseWord.split(' ') .reverse() .join(' ')
  return lowerCaseWord === reversedWord
}

让我们来将这个代码改为TypeScript,将我们的文件后缀改为.ts即可。

通过分析,我们可以知道,该函数的参数 word 的类型应该是 string ,该函数的返回值类型应该是 boolean,因此我们在TypeScript中声明它们的类型,像这样:

function isPalindrome(word: string) : boolean {
  const lowerCaseWord = word.toLowerCase()
  const reversedWord = lowerCaseWord.split(' ').reverse().join(' ')
  return lowerCaseWord === reversedWord
}

下面我们分别使用正确的参数类型和错误的参数类型进行测试,可以看到,在TypeScript编译之前,VsCode就给我们抛出了错误:

TypeScript Error

这就是TypeScript的优点,它会迫使你对代码更加严格和明确。

Types

再上一个例子中,我们明白了如何为我们的函数参数,返回值指定基本类型,下面让我们学习一些更复杂的方式:Type,它可以以一种更好的方式描述我们的数组或对象:

type User = {
  username: string;
  email: string;
  name: string;
  age: number;
  website: string;
  active: boolean;
};

当我们将不正确的类型赋给类属性,或者确实了某个属性,TypeScript 都会给我们提示:

const user: User = {
  username: "czantany",
  email: "carlos@milkzoft.com",
  age: "22", // 类型不正确
  // 缺少 name 属性
  website: "http://www.js.education",
  active: true,
};
image-20220130233343709

如果你需要一个可选的属性,那么你可以在变量名后面添加一个问好,表示可选:

type User = {
  username: string
  email: string
  name: string
  age?: number // `?` 说明 age 是可选的
  website: string
  active: boolean
}

虽然type后面的变量名是随意的,但我们约定俗成的希望在变量前添加 T ,如将 User 改为 Tuser,以表示该变量是 Type

Interfaces

Interface 和 type 十分相似,但语法是不同的:

interface User {
  username: string;
  email: string;
  name: string;
  age?: number;
  website: string;
  active: boolean;
}

虽然 interface 后面的变量名是随意的,但我们约定俗成的希望在变量前添加 I ,如将 User 改为 Iuser,以表示该变量是 Interface

Interfaces 可以被继承、实现、聚合(be extended, implemented, and merged)

Extending

和常规的类继承不同,TypeScript 一共有三种继承方式,其规则是:

  • 使用&继承 types,并使用=赋值
  • 使用 extends 继承 interface,和常规类继承一致

Extending an interface:

// Extending an interface
interface IWork {
  company: string;
  position: string;
}
interface IPerson extends IWork {
  name: string;
  age: number;
}

Extending a type

// Extending a type
type TWork = {
  company: string;
  position: string;
};

type TPerson = TWork & {
  name: string;
  age: number;
};

Extending an interface into a type

// Extending an interface into a type
interface IWork {
  company: string;
  position: string;
}
type TPerson = IWork & {
  name: string;
  age: number;
};

Implementing

类可以实现interface 或者 type

// Implementing an interface
interface IWork {
  company: string;
  position: string;
}
class Person implements IWork {
  company: "BUAA";
  position: "beijing";
  name: "flag";
  age: 18;
}

// Implementing a type
type TWork = {
  company: string;
  position: string;
};
class Person2 implements TWork {
  company: "BUAA";
  position: "beijing";
  name: " flag";
  age: 18;
}

不能继承联合类型(union) 的 type,仅作了解即可

Declaration merging

不同于type,interface可以被多次定义,且每一次都会与之前的定义进行合并,如下所示:

interface IUser {
  username: string;
  email: string;
  name: string;
  age?: number;
  website: string;
  active: boolean;
}
interface IUser {
  country: string;
}
const user: IUser = {
  username: "czantany",
  email: "carlos@milkzoft.com",
  name: "Carlos Santana",
  country: "Mexico",
  age: 33,
  website: "http://www.js.education",
  active: true,
};

当你需要扩展你的interface时,你只需要重定义即可,新的属性会被合并到之前的定义中,这是十分有用的。

Summary

在第一章中,我们学习了一些基本的概念:

  • 我们知道了如何编写声明性代码,也清楚地了解了我们创建的组件和React用来在屏幕上显示实例的元素之间的区别。
  • 我们了解了选择将逻辑和模板放在一起的原因,以及为什么这个不受欢迎的决定是React的一大胜利。
  • 我们讨论了JavaScript生态系统中经常感到疲劳的原因,但我们也看到了如何通过遵循迭代方法来避免这些问题。
  • 我们学习了如何使用TypeScript来创建一些基本类型和接口。
  • 我们了解了新的create-react-app CLI是什么

这些概念对于接下来的学习非常重要,现在我们准备开始编写一些真正的代码。在下一章中,您将学习如何使用JSX/TSX代码,并应用非常有用的配置来改进您的代码风格。