9102年了, 学习React其实完全可以从react-hooks开始, 这也是我写这篇文章的初衷。
另外我也希望能用比较简洁的语言来解释一些相关的原理, 不过本文还是会以如何使用为主, 所以比较适合想了解个大概后快速上手去写的人。
本文主要内容是解释如何基于Typescript来写React组件, 需要你有javascript基础, 对html有一定的认识。
准备环境
开发环境
已经装过的可以跳过
- 安装node 8.10以上的版本, 最好就用最新版
- 安装yarn
- 修改yarn的registry, 执行下列命令即可
yarn config set registry http://registry.npm.taobao.org/
- 安装vscode, 这是对ts支持最好的editor了, 没必要用其他的
创建项目
你可以使用已有的项目, 或者parcel, webpack等其他构建工具。
$ mkdir react-quickstart && cd react-quickstart
$ yarn create umi
为了使项目结构简单一点,这里选择app
,并选择使用typescript,最后勾选antd。
然后将src/layouts
和 src/pages
下的文件都删掉,然后在src/pages
下创建index.jsx
,加上以下内容。
import * as React from "react";
export default function Index() {
return <span>hello world</span>;
};
- yarn install
- yarn start
打开 http://localhost:8000/, 就能在屏幕上看到 hello world 了。
React入门
认识组件
使用react你唯一需要做的事情就是写组件, react里的组件和其他UI框架差不多, 就是封装了一系列UI和行为的代码.
上面我们在src/pages/index.jsx
里定义了一个页面, 这个页面本身就是一个组件. 在react里定义一个组件的方法如下
// FC is Function Component
function Index() {
return <span>hello world</span>;
};
没错, 组件就是一个函数(让我们先忽视类的写法), 对于没有状态的组件我们可以将其当作一个纯函数. 比如下面我们用ts来申明组件的参数:
const Index: React.FC<{ name: string; }> = (props) => {
return <span>hello {props.name}</span>;
};
这个组件有一个 参数 用户名(输入), 以及对应返回的Virtual DOM(输出).
Inversion of Control
React是一个典型的IoC框架, 我们只要写好组件交付给React, 它能帮我们处理好UI(HTML DOM)的创建, 更新和销毁.
组件 React
Properties -------> Virtual DOM --------> HTML DOM
JSX(TSX)
上面例子里每个组件都返回了类似HTML的表达式, 这就是JSX.
我觉得官方文档讲得挺清楚了,你可以花几分钟看一下。另外这篇文档讲得比较详细,建议简单看一遍,遇到了问题可以多查阅。
你只要理解下面两点就能完全掌握JSX:
- JSX本身就是JS表达式(Expression)
- JSX里可以在参数部分(包括children参数)用花括号(
{
和}
)内嵌JS表达式。
JSX范式
下面列了一些比较常见的JSX写法
条件渲染
在组件内
`
jsx
function MyComponent(props) {
if (props.visible) {
return ‘Hi, I\’m visible.’;
}return ‘I\’m invisible now.’;
}
2. 在JSX内,会用到花括号嵌入JS表达式来实现。由于JS中`if..else..`不是表达式,所以会使用逻辑运算符(`&&`或`||`)和三元表达式(`.. ? .. : ..`)
```jsx
function Greeting(props) {
return (
<span style={{ color: 'red' }}>
{props.lang === 'janpanese' ? 'こんにちは' : 'Hello'}
{props.emphasis && '!'}
</span>
);
}
<Greeting /> // <span>Hello</span>
<Greeting lang="janpanese" /> // <span>こんにちは</span>
<Greeting lang="janpanese" emphasis /> // <span>こんにちは!</span>
列表渲染
把一系列数据渲染一个列表或者表格也是相当常见的需求,在JSX里我们可以使用数组上的map
方法来实现
function Ranking(props) {
return (
<div>
{props.value && props.value.map((name, index) => (
// 这里需要给列表每一项加上一个id
<div key={index}>{index + 1}. {name}</div>
))}
</div>
)
}
<Ranking /> // <div></div>
<Ranking value={[]} /> // <div></div>
<Ranking value={['dio', 'kars', 'kirayoshikage', 'Diavolo']} />
/* output:
<div>
<div>1. dio</div>
<div>2. kars</div>
<div>3. kirayoshikage</div>
<div>4. Diavolo</div>
</div>
*/
这个例子有两个小技巧:
- 利用
&&
来判空,兼容value
未传入的情况(此时value === undefined
)。因为这里没有定义参数类型,使用者可能会传入任何值,所以需要做一些简单处理。不过在使用typescript后就不存在这种问题了。 map
中传入的箭头函数其实是以下函数的缩写,(name, index) => { return ( <div>{index + 1}. {name}</div> ); }
为什么渲染列表时需要传入key这个参数
这是由于React的diff算法所决定的,我们需要给列表中每项一个在当前列表内的唯一ID。你可以参考这篇文章来选择key。
封装复杂的渲染逻辑
当你在一个组件内的JSX写得越来越复杂时,可以将部分逻辑抽离出来单独写一个组件,如果你认为这些逻辑没什么可复用性,再写一个组件要传很多参数比较麻烦,那么单独抽离成一个函数也可以。
function ComplexComponent() {
// some state code...
const displayType = ...;
function renderChart() {
// codes
}
function renderTable() {
// codes
}
return (
<div className="some-layout-style">
<div>
{ /* some complex jsx */ }
</div>
{displayType === 'chart' ? renderChart() : renderTable()}
</div>
)
}
有时候也可以将组件里一些重复用到的JSX写到一个函数内
function TopOfTheYear(props) {
function renderRank(value) {
return value && value.map((name, index) => (
<div>{index + 1}. {name}</div>
));
}
return (
<div>
<div>ANIMES:</div>
{renderRank(props.animes)}
<div>MOVIES:</div>
{renderRank(props.movies)}
</div>
)
}
出现嵌套列表时也可以递归调用,比如下面递归渲染的树
function Tree(props) {
function renderTreeNodes(nodes) {
return nodes.map((node, i) => (
<div className="children">
{node.name}
{nodes.children && (
<div className="children">
{renderTreeNodes(nodes.children)}
</div>
)}
</div>
))
}
return (
<div className="tree-root">
{props.value && renderTreeNodes(props.value)}
</div>
);
}
通常我们在刚写jsx时会有疑惑,为什么需要import react?
因为JSX被编译出来的JS表达式会使用React
,比如下面的例子
// 编译前
const virtualDom = (
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
);
// 编译后
const virtualDom = React.createElement(
MyButton,
{ color: 'blue', shadowSize: 2 },
'Click Me'
);
这里编译后不会自动帮我们加上import,所以需要我们手动加了。如果你不喜欢手动写import,可以通过配置打包工具来少些几行代码,比如webpack的配置方法。
这个其实在上面的文档里就有解答,遇到了问题多看看文档。
状态
上面我们写的都是纯函数组件,但是大部分情况组件都会有内部状态,下面让我们看看怎么给组件添加状态。
PS: 有状态的组件称为函数组件
引入状态对应的API是React.useState
,看下面的例子
import { useState } from 'react';
function Counter() {
// 这里声明了我们要使用一个初始值为 0 的状态
const [count, setCount] = useState(0);
// count只有在第一次渲染时使用初始值,之后都是内部维持的状态
// setCount用来更新状态
return (
<div>
<div>count: {count}</div>
<button
onClick={() => setCount(count + 1)}
>+</button>
</div>
)
}
复杂状态
有时候状态可能是个复杂的对象,和上面用法差不多,但要注意的是你只更新对象上的一个变量时,也需要生成一个新对象。
只是简单更新单层对象可以参考以下范式
const obj1 = { a: 1, b: 2 };
// 解构
const obj2 = { ...obj1, b: 3 }; // { a: 1, b: 3 }
// Object.assign方法
const obj3 = Object.assign({}, obj1, { a: 2 }); // { a: 2, b: 2 }
例子
import { useState } from 'react';
function Counter() {
const [state, setState] = useState({ count: 0, other: 'a' });
return (
<div>
<div>count: {state.count}</div>
<button
onClick={() => setState({
// 这里setState会覆盖掉之前的状态,所以要保留other的话,需要将新旧对象合并到一起
...state,
count: state.count + 1,
})}
>+</button>
</div>
)
}
分离逻辑(useReducer)
todo
https://reactjs.org/docs/hooks-reference.html#usereducer
多个状态
useState
可以在一个组件中创建任意数量的状态,但你不能在条件、循环和嵌套函数里调用。
import { useState } from 'react';
function Counter(props) {
// 你只能在函数的顶级调用
const [c, s] = useState(0);
// error: 不能放到任何条件block内
if (props.visible) {
const [c, s] = useState(0);
}
// error: 同上
const [c, s] = props.visible ? useState(0) : useState(99);
function renderContent() {
// error: 不能在嵌套函数中调用
const [c, s] = useState(0);
}
// ...
}
你不用死记这些,因为有eslint会帮你发现这些错误,你只要遇到了知道是什么原因就行。
为什么要按固定的顺序调用?
函数组件的状态是由React帮你维护的,每次调用useState
,React就会找到对应的状态返回给你,这是很经典的依赖注入的思想。
下面的例子相信很容易理解
// react 内部的状态列表 internal_state = ['a', 'b', 'c']
const [a] = useState('a'); // 返回 internal_state[0]
const [b] = useState('b'); // 返回 internal_state[1]
const [c] = useState('c'); // 返回 internal_state[2]
// 如果某些原因导致顺序发生了变化,拿到的值就不正确了
const [a] = useState('a'); // 返回 internal_state[0]
const [c] = useState('c'); // 返回 internal_state[1]
const [b] = useState('b'); // 返回 internal_state[2]
useEffect(组件生命周期)
React hook
上面提到的useState
、useReducer
以及接下来介绍的useEffect
都是React hook
,hook你可以理解为函数组件内可以使用的一系列工具,通常 约定 以use
开头。
如何监听生命周期
如果你之前接触过React,那你可能知道一些很常用的生命周期方法,像componentDidMount
(组件第一次渲染后)、componentDidUpdate
(组件每次更新后)等等。
这些生命周期方法对于一个组件是非常重要的,在函数组件里我们可以使用useEffect
来实现。
下面是个如何监听页面滚动的例子,我们需要在组件挂载到节点后添加事件监听,并在组件销毁时移除事件监听以免内存泄漏。
import { useEffect } from 'react';
function MyComponent() {
// 滚动事件的监听函数
function scrollHandler() {}
// 基本等同于componentDidMount
useEffect(() => {
document.addEventListener('scroll', scrollHandler);
// 这里返回的函数会在组件销毁前被调用
return () => {
document.removeEventListener('scroll', scrollHandler);
};
}, []);
return <div>...</div>
}
useEffect
useEffect
会接收两个参数,第一个参数是一个函数A,其会在适当的时机被调用,该函数如果返回了一个函数B,函数B被称为clean up
函数,B会在下一次A被调用前或者组件销毁前被调用。
第二个参数用来提示A的调用时机,通常有3种情况:
传入
undefined
(等同不传):这种情况会在每次渲染时调用A,可以理解为在组件首次渲染或者更新时调用。这种情况使用得不多,通常用来手动跟踪某些值和节点的变化。
传入
[]
:这种情况会在组件首次渲染时调用。通常用于组件内发起一次性的请求。
const [result, setResult] = useState(); const [loading, setLoading] = useState(true); useEffect(() => { // 这里你不能直接使用async function,因为它会返回的Promise会被误认为cleanup函数 async function fn() { setResult(await fetchSomething()); setLoading(false); } fn(); }, [])
传入非空数组:在2的基础上,如果数组中任意值发生了变化,就会去调用。
通常用来监听某些状态的变化并执行一些副作用函数,比如将状态保存到浏览器。
const [value, setValue] = useState(() => localStorage.getItem('VALUE_ID')); useEffect(() => { // 每次value变化就会将其持久化到浏览器 localStorage.setItem('VALUE_ID', value); }, [value]);
使用typescript来写React组件
为什么要用typescript?
没有类型辅助写大中型项目(尤其在多人协作方面)是件心智负担很重的事, typescript是目前使用最为广泛的 compile to javascript 语言. 当然你也可以使用 flow, reasonml, elm 等语言, 不过这些不是本文的目标。
如果只想用js, 那么只需要将文件后缀改为 jsx 并且去掉所有类型即可。
本节只会解释本文会用到的ts特性, 深入可以参考推荐阅读里的内容。
你需要知道的Typescript
ts里大部分时间不需要显式声明类型, 因为它能自动推断。
// 基础类型
const a: number = 1;
const b: string = 'abc';
const c: boolean = true;
const d: undefined = undefined;
const e: null = null;
// 函数类型
function parseInt(input: string, radix?: number): number { /* implement code */ }
// 箭头函数类型
const parseInt: (input: string, radix?: number) => number = (input, radix) => { /* implement code */ };
// Union Type
let union: number | string | undefined = 1;
union = undefined; // ok
union = 'ok'; // ok
// 字面量类型
const f: 'jotaro' | 'giorno' = 'giorno';
const g: 1 | 2 = 3; // ts will throw error
// 对象字面量类型
const obj: { field: string } = { field: 'abc' };
// 接口
interface A {
message?: string; // '?'将会给类型添加undefined, 这里相当于 string | undefined
}
const h: A = { message: 'now' };
const i: A = {}; // message is undefined
const j = { message: 'now' }; // 如果我们不加类型定义, 这里j会被推断为 { message: string }
Type Compatibility
ts里如果两个接口的每项参数的类型是匹配的那么, 这两个接口就是匹配的. 比如下面两个接口在ts里其实没有什么区别
interface Point {
x: number;
y: number;
}
interface Rectangle {
x: number;
y: number;
}
这个特性主要对于经常出现的对象字面量很有帮助
interface A {
message?: string;
}
let obj = { message: 'now' }; // 这个对象的类型是 { message: string }
// 虽然这个类型和A不是同一个类型,但是它们是兼容的, 所以这里可以将这个对象赋值给类型为 A 的变量 h.
const h: A = obj;
Union Type和其他字面量类型也是很符合直觉的
const a = 'a';
const b: 'a' | 'b' = a; // ok
const c: string | number = b; // ok
const d: number = c; // error
if (typeof c === 'number') {
const e: number = c; // ok, c is number type here
}
还有其他比如函数、枚举、泛型的类型匹配可以看官方文档。因为这些比较少用到,所以碰到了再看文档就行。
泛型
和其他语言差不多, 在本文里就不深入解释了
import
通常你会看到在很多项目的tsx文件里会这么import react
import * as React from 'react';
这种写法是因为react本身是没有export default
,而且ts不像js会默认去兼容commonjs的写法,所以你需要import entire module。如果你不喜欢这种写法,可以修改tsconfig.json
来兼容。
编写可维护的组件
todo
推荐阅读
常用组件和库
- ant design: UI组件
- lodash: 函数库
- moment: 时间工具