01-React 17 Design Patterns Best Practices
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
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的优点,它会迫使你对代码更加严格和明确。
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,
};
如果你需要一个可选的属性,那么你可以在变量名后面添加一个问好,表示可选:
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代码,并应用非常有用的配置来改进您的代码风格。