第三章 开始在React中使用TS

  • 创建一个React和TypeScript项目
  • 创建一个类组件
  • 处理类组件的事件
  • 类组件的状态
  • 类组件声明周期方法
  • 创建一个函数组件

创建一个React和TypeScript项目

使用create-react-app

create-eract-app是一个npm包的命令行工具,用于快速创建React和TypeScript应用。

1
npx create-react-app my-react-ts-app --typescript

项目创建后,添加TSLint,

1
2
cd my-react-ts-app
npm install tslint tslint-react tslint-config-prettier --save-dev

添加文件tslint.json,包含一些规则,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"extends": ["tslint:recommended", "tslint-react", "tslintconfig-prettier"],
"rules": {
"ordered-imports": false,
"object-literal-sort-keys": false,
"no-debugger": false,
"no-console": false,
},
"linterOptions": {
"exclude": [
"config/**/*.js",
"node_modules/**/*.ts",
"coverage/lcov-report/*.js"
]
}
}

启动,

1
npm start

创建一个简单的React component

1
2
3
4
5
import * as React from "react";

const App: React.SFC = () => {
return <h1> My React App!</h1>;
}

React.SFC是一个TypeScript的React类型,它不包含任何内部状态。

1
2
3
4
5
6
import * as React from "react";
import * as ReactDOM from "react-dom";
const App: React.SFC = () => {
return <h1>My React App!</h1>;
};
ReactDOM.render(<App />, document.getElementById("root") as HTMLElement);

添加webpack

(略)

项目目录结构

(略)

创建一个基本的类组件

1
2
3
4
5
6
7
8
9
10
import * as React from "react";

class Confirm extends React.Component {
public render() {
return (
);
}
}

export default Confirm;

render方法决定了该组件需要展示的内容。我们用JSX来定义需要展示的内容。简单来说,JSX就是HTML和JavaScript的混合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public render() {
return (
<div className="confirm-wrapper confirm-visible">
<div className="confirm-container">
<div className="confirm-title-container">
<span>This is where our title should go</span>
</div>
<div className="confirm-content-container">
<p>This is where our content should go</p>
</div>
<div className="confirm-buttons-container">
<button className="confirm-cancel">Cancel</button>
<button className="confirm-ok">Okay</button>
</div>
</div>
</div>
);
}

怎么使用Confirm组件?在App.tsx中,

1
2
3
4
5
6
7
8
9
import Confirm from './Confirm';

...
<div className="App">
<header className="App-header">
...
</header>
<Confirm />
</div>

JSX

JSX看起来很像HTML,它不是有效的JavaScript,我们需要一个处理步骤将其转换为JavaScript。

打开浏览器,进入https://babeljs.io/repl,输入下面内容,

1
<span>This is where our title should go</span>

右侧会获得编译之后的JS文件,

1
2
3
4
5
React.createElement(
"span",
null,
"This is where our title should go"
);

React.createElement有三个参数,

  • 元素类型,可以是一个HTML标签,一个React组件类型,或一个React 代码段
  • 属性对象
  • 子类或内容

例如,

1
2
3
<div className="confirm-title-container">
<span>This is where our title should go</span>
</div>

最终会被编译为,

1
2
3
4
5
6
7
8
9
React.createElement(
"div",
{ className: "confirm-title-container" },
React.createElement(
"span",
null,
"This is where our title should go"
)
);

现在看是有意义的,但目前仅由HTML构筑。加点JavaScript代码看看,

1
2
3
4
5
6
const props = {
title: "React and TypeScript"
};
<div className="confirm-title-container">
<span>{props.title}</span>
</div>

它会被编译为,

1
2
3
4
5
6
7
8
9
10
11
12
var props = {
title: "React and TypeScript"
};
React.createElement(
"div",
{ className: "confirm-title-container" },
React.createElement(
"span",
null,
props.title
)
);

更进一步,让字面量props为空,

1
2
3
4
const props = {};
<div className="confirm-title-container">
<span>{props.title ? props.title : "React and TypeScript"}</span>
</div>

内嵌调用原封不变,

1
2
3
4
5
React.createElement(
"span",
null,
props.title ? props.title : "React and TypeScript"
)

因此,为什么我们使用className属性而不是class?现在知道JSX会编译为JavaScript,由于class是JavaScript的关键字,在JSX包含class属性会发生冲突。因此React使用className代替CSS引用。

Component props

目前,组件Confirm的标题和内容是硬编码的。需要将这些引用属性以组件形式接收

  1. 首先,我们需要为我们的props定义个TypeScript类型。我们将会用到一个接口,
1
2
3
4
interface IProps {
title: string;
content: string;
}
  1. 将该IProps类型以尖括号引入定义中,
1
class Confirm extends React.Component<IProps>

React.Component被称为泛型类。泛型类允许类型允许在内部传递使用。在我们的例子中,我们传递了IProps接口。

  1. 然后我们的类内使用this.props.propName。在我们的JSX文件中,可以直接引用这些属性,取代硬编码的方式:
1
2
3
4
5
6
7
8
9
...
<div className="confirm-title-container">
<span>{this.props.title}</span>
</div>
<div className="confirm-content-container">
<p>{this.props.content}</p>
</div>
...

目前编译不过,因为Confirm组件现在要求传入titlecontent属性,

修改为,

1
2
3
4
<Confirm
title="React and TypeScript"
content="Are you sure you want to learn React and TypeScript?"
/>

Optional props

接口Props的内容可以定义为可选属性,

1
2
3
4
5
6
interface IProps {
title: string;
content: string;
cancelCaption?: string;
okCaption?: string;
}

Default prop values

当组件被初始化,可以被组件添加默认props属性值。它通过一个称为defaultProps的静态对象字面量实现。

  1. 创建默认的cancelCaptionokCaption
1
2
3
4
5
6
7
class Confirm extends React.Component<IProps> {
public static defaultProps = {
cancelCaption: "Cancel",
okCaption: "Okay"
};
public render() { ... }
}

如果要覆盖默认属性,补充具体属性值即可,

1
2
3
4
5
6
<Confirm
title="React and TypeScript"
content="Are you sure you want to learn React and TypeScript?"
cancelCaption="No way"
okCaption="Yes please!"
/>

带有默认值的可选属性便于组件使用,这样大部分通用的配置可以自动装配起来,不用逐个指定。

处理类组件事件

事件存在于多数编程语言中。以允许我们执行特定逻辑。

基础事件句柄

所有的本地JavaScript事件都可以在JSX中处理。JSX允许我们通过属性来调用这些事件函数。本地事件名会被带上前缀on以峰驼方式传递。因此,例如在JS中的属性事件是click,在JSX则对应onClick

要查看所有可用事件列表,可以前往node_modules/@types/react文件夹的index.d.ts文件

  1. 首先是我们需要处理按钮上的click事件,对应上为,
1
<button className="confirm-ok" onClick={this.handleOkClick}>...</button>
  1. 创建这个handleOkClick方法,
1
2
3
private handleOkClick() {
console.log("Ok clicked");
}

The this problem

在事件的处理上承受来自JavaScript的经典this问题。我们在事件处理上获取不到引用,譬如,

1
2
3
private handleOkClick() {
console.log("Ok clicked", this.props):
}

点击按钮,会出现undefined!!原因是this代表的是当前这个事件,而不是我们的类!

一种解决方法是,将handleOkClick方法改为箭头函数(arrow function)。

arrow function相当于一个表达式。它不会创建自身的this——这样解决了this的问题。

我们把原来的方法改一改,

1
2
3
private handleOkClick = () => {
console.log("Ok clicked", this.props);
}

现在再次点击按钮,程序正常了。

Function props

有时候需要在组件消费者(component)中传递事件处理逻辑。

  1. 修改对应的IProps接口,对应函数类型属性,
1
2
3
4
5
6
7
8
interface IProps {
title: string;
content: string;
cancelCaption?: string;
okCaption?: string;
onOkClick: () => void;
onCancelClick: () => void;
}

然后在消费方引用函数属性,

1
2
3
4
5
<Confirm
...
onCancelClick={this.handleCancelConfirmClick}
onOkClick={this.handleOkConfirmClick}
/>

类组件状态

状态是一个对象,它决定了组件的行为和渲染。我们需要在我们的app中引入状态,以管理我们Confirm窗口打开或关闭。

State的定义和Props类型,首先我们需要创建一个接口,

1
2
3
interface IState {
confirmOpen: boolean;
}

接着传递React.Component的第二个泛型参数中,

1
class App extends React.Component<{}, IState>

Initializing the state

定义的状态需要被初始化,初始化动作在构造函数中实现,

1
2
3
4
5
6
constructor(props: {}) {
super(props);
this.state = {
confirmOpen: true,
};
}

state被存放在组件类内的一个私有属性中。以及可以在组件内被使用。

1
2
3
4
<Confirm
open={this.state.confirmOPen}
...
/>

Changing state

状态的改变不能直接访问控制,

1
2
3
private handleOkConfirmClick = () => {
this.state.confirmOpen = false;
};

它会出现错误消息说状态是read-only!的。我们需要使用setState方法来改变状态。

1
2
3
private handleOkConfirmClick = () => {
this.setState({ confirmOpen: false });
};

我们仅能在构造函数中初始化State,其它类组件任何地方都不能初始化状态。以及,状态的更改,仅能在该组件内调用setState实现。

Class component life cycle methods

生命周期允许我们在特定点做某些处理。

componentDidMount

当一个组件被插入到DOM中时,componentDidMount被调用。下面是一些该方法常见的用例:

  • 调用web service以获取某些数据
  • 添加事件监听
  • 初始化计时
  • 初始化第三方库
1
2
3
4
5
private timer: number = 0;

public componentDidMount() {
this.timer = window.setInterval(() => this.handleTimerTick(), 1000);
}

componentWillUnmount

当组件从DOM内被移除时触发componentWillUnmount,下面是常见的用例,

  • 移除事件监听器
  • 取消激活的网络请求
  • 移除计时器
1
2
3
public componentWillUnmount() {
clearInterval(this.timer);
}

getDerivedStateFromProps

每次组件被渲染时,触发getDerivedStateFromProps。它是一个组件的静态方法,返回改变的状态或返回null。

1
2
3
4
public static getDerivedStateFromProps(props: {}, state: IState) {
console.log("getDerivedStateFromProps", props, state);
return null;
}

getSnapshotBeforeUpdate and componentDidUpdate

shouldComponentUpdate

Creating a function component

函数组件是从JavaScript函数实现的。

Creating a basic function component

1
2
3
const Confirm: React.SFC<IProps> = (props) => {
...
}

完整示例如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import * as React from "react";

const ComponentName: React.SFC<IProps> = props => {
const handler = () => {
...
};

return (
<div> Our JSX</div>
);
};

ComponentName.defaultProps = {
...
};

export default ComponentName;

Stateful function components

富状态函数组件,

1
2
3
4
5
6
7
8
const Confirm: React.SFC<IProps> = props => {
const [cancelClickCount, setCancelClickCount] = React.useState(0);

const handleOkClick = () => {
props.onOkClick();
};
...
}