虽然发现React是有中文网的,但是奈何一直打不开中文文档,即使我已经开了VPN,自己现在又习惯性的想看更专业的官方文档,国内的一些教程实在看不下去了,所以想着自己尝试翻译他的文档,帮助一些和我一样有强迫症患者的朋友。


官网就是官网,教程真的是牛逼,react教程分为两种,实践教程(practical tutorial)和概念教程(guide to main concepts),都很好,我这里就先翻译实践教程吧!


实践教程(practical tutorial)

在开始实践教程之前(Before We Start the Tutorial)

在这个教程中,我们将做一个小游戏,你千万别被吓到,你怎么这么快就能做一个游戏了,我要说的是,你并不是在做一个很厉害的游戏,我们也只是给你一个这样的机会,在这个方法的教程中,你将会学习到用react构建基础的应用并且熟练地掌握它,它也将让你充分理解React。

建议
这个教程被设计为那些想要通过亲自动手边做边学的人,如果你更喜欢去一步一步学习概念,你可以去另一套教程里,一会发现这两套教程可以互补的学习,大家可以取他们各自的长处!

这个教程可以被分为下面几个部分

  • 教程构建 将给你一个如何开始这个教程的指导
  • 概述 将教你一些React的基础内容:components, props和state
  • 小游戏项目 将教你一些在React开发过程中最通用的一些技能
  • 历史回流 将让你对React有更深的理解,体会到React强大之处

你没必要立刻掌握以上所有的部分,你可以尝试先去学你能学会的部分,没必要纠结是否自己基础是否太薄弱。

有时跟着教程拷贝代码是好事,但我们还是建议你去一步一步的去手写,那将帮助你更好的记忆和更深的理解!

我们在做什么(这部分可以不看吧,有点啰嗦了)

在这个教程中,我们将为你展示如何去用React开发一个交互的小游戏。

你可以先看一下游戏做出后最后的成果,如果你不能理解代码,还不熟悉它如何做出来的,别担心,这个教程的目标就是教会你这些东西,不然你可以走了。

我们建议你继续教程之前先熟悉一下这个游戏,你可以注意到在游戏面板的右面有一个数字列表,他将记录你游戏的每一步,并且可以实时更新。

熟悉了之后就Ok了,下面我们一下来开发这个游戏!

前提条件(Prerequisites)

我们假设你已经熟练掌握了HTMl和JavaScript了,虽然有其他语言基础也可以,但那些基础还是要掌握的,我们也假设你熟悉了一些例如functions`objectsarrays这些Js里面的概念,并且有一些对classes`有一定的理解。

如果你需要去重新学习JavaScript,我们推荐阅读这个教程,注意:我们也使用一些ES6的语法,例如尖头函数、classesletconst,你可以使用Babel REPL核实ES6代码规范。

教程构建(Setup for the Tutorial)

有两个方法学习这个教程:

  • 你可以在浏览器中再现编写代码
  • 也可以搭建本地的开发环境

方法一:浏览器在线编写

这是很快的入门方法

首先打开Starter Code,这里还没有把游戏显示出来,我们可以在这里在线编辑学习。

确定是痛这种方法的话,我们可以跳过下面第二种方法去看概述部分了

方法二: 搭建本地开发环境

虽然我们这个教程中并不需要这种方法,但他也很重要!
(This is completely optional and not required for this tutorial!,原文显得很不重要一样)

这个方法需要更大的工作量,但是可以在本地选择自己的编辑器来学习本套课程,下面是步骤:

  1. 确定你已经安装了Node.Js
  2. 跟着下面安装指导来创建新的工程

    1
    2
    npm install -g create-react-app
    create-react-app my-app
  3. 删除新项目中src/目录下所有的文件(文件夹保留)

    1
    2
    cd my-app
    rm -f src/*
  4. src/文件夹中新建一个名为index.css的文件,用于编写css代码

  5. src/文件夹中新建一个名为index.js的文件,用于编写js代码

  6. index.js中添加下面三行代码

    1
    2
    3
    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';

现在你可以运行npm start,浏览器打开http://localhost:3000,你可以看见一个空的页面,这样,本地的开发环境就搭建好了。

寻求帮助

如果你在构建环境的过程中有什么问题,可以直接找我~~~

原文
If you get stuck, check out the community support resources. In particular, Reactiflux Chat is a great way to get help quickly. If you don’t receive an answer, or if you remain stuck, please file an issue, and we’ll help you out.

概述(Overview)

React是什么?(What Is React?)

React是开源、高效、灵活的Js框架,它能够让你李用一个个脱离的片段(被叫做组件components)来构建出一个复杂的UI。

React有一部分不同种类的组件,但是它们都继承自React.Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ShoppingList extends React.Component {
render() {
return (
<div className="shopping-list">
<h1>Shopping List for {this.props.name}</h1>
<ul>
<li>Instagram</li>
<li>WhatsApp</li>
<li>Oculus</li>
</ul>
</div>
);
}
}

// Example usage: <ShoppingList name="Mark" />

我们可以看到类似XML标签的代码,我们使用组件去告诉React我们将在屏幕中显示什么,当我们数据改变React也将高效的更新这个组件。

这里,ShoppingList是一个组件( React component class, or React component type),组件里包含一些参数,叫做props(propertie的缩写),并且通过render方法返回一个试图层(hierarchy of views).

render方法返回一个你想在网页中看到的“描述”,React拿到这个“描述”并且展示出来,大部分React开发者使用一种特殊的语法“JSX”来写render的内容,极其方便。在JSX语法中
<div />句式被转换成React.createElement('div'),这样我们可以在JS代码中方便的写入类似HTML标签的语法了。
React.createElement('div')render内容Example:

1
2
3
4
return React.createElement('div', {className: 'shopping-list'},
React.createElement('h1', /* ... h1 children ... */),
React.createElement('ul', /* ... ul children ... */)
);

完整的例子(可见,很繁琐)

如果你想了解更多createElement()的内容,请查阅API文档,在实际开发中我们不常使用到它,而是用更为方便的JSX语法。

JSX自带Javascript的全部功能,你可以把任何JS表达式放在JSX的代码块中,每个React组件是一个Js对象,你可以在其中使用一些变量和函数。

上面的ShoppingList组件仅仅在DOM树中渲染了<div /><li />,你也可以渲染出自定义的React组件。比如说,你可以通过写<ShoppingList />来显示ShoppingList的全部内容,每个React组件被独立的分离,这样,你就可以通过简单的组件构建出一个复杂的UI界面。

浏览 the Starter Code(Inspecting the Starter Code)

如果你是在线学习编写代码,打开这个代码页面:Starter Code,如果你是在本地编写代码,打开src/index.js

这个代码是你以后进行后续编写的基础,我们已经提供了CSS样式,你可以专注于学习React来开发这个游戏。

通过阅读这段代码,你会发现其中有三个组件:

  • Square
  • Board
  • Game

Square组件仅渲染了单个的<button>,Board组件渲染了9个Square组件,Game组件渲染了一个board组件和一个以后将要修改了占位代码,现在还没有能够交互的组件。

通过props传递数据(Passing Data Through Props)

趁热打铁!让我们试着将数据从Board组件传递到Square组件。

在Board组件的renderSquare方法中,修改代码让一个叫做value的变量传递到Square组件。

1
2
3
4
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />;
}

将Squarerender方法中的{/* TODO */}修改为{this.props.value}

1
2
3
4
5
6
7
8
9
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value}
</button>
);
}
}

修改之前界面是:

界面视图
界面视图

修改之后,你将可以在每个Square组件中看见一个数字:

界面视图
界面视图

看此部分完整代码

祝贺你!你已经成功将数据从Board组件(父zujian)传递到Square组件(子组件),props传递是React中从父到子的数据流动的方式。

制作一个交互组件(Making an Interactive Component)

功能:当我们点击时,能将Square组件中的值变为“X”。
首先,改变组件中的render()方法:

1
2
3
4
5
6
7
8
9
class Square extends React.Component {
render() {
return (
<button className="square" onClick={function() { alert('click'); }}>
{this.props.value}
</button>
);
}
}

现在点击一个Square组件,你将看到一个alert框。

Note
为了保存类型并且避免对this不能理解,我们将使用尖头函数的句式(arrow function syntax

1
2
3
4
5
6
7
8
9
class Square extends React.Component {
render() {
return (
<button className="square" onClick={() => alert('click')}>
{this.props.value}
</button>
);
}
}

请看这边onClick={() => alert('click')},我们传递一个函数作为onClick的参数,这个函数将在我们点击的时候执行,大家通常会忘了写() =>而写成onClick={alert('click')},如果这样的话,大家可以看看会发生什么。

下一步,我们想要让Square“记住”我们点击的行为,并且在其中放入一个“X”字符。要让组件“记住”,
我们要用到state

React组件可以通过在构造函数中设置this.state来定义state,this.state被认为是React组件中被私有的。让我们在this.state中储存当前Square的value,点击的时候能瘦改变。

首先,写一个构造函数去初始化state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}

render() {
return (
<button className="square" onClick={() => alert('click')}>
{this.props.value}
</button>
);
}
}

Note
JavaScript classes中,当定义一个子类的时候,你需要去调用super方法。所有的React组件的类都应该有一个constructor并在其中调用super(props)

现在我们将改变Square的render方法去实现当我们点击时展示当前的state的值:

  • <button>标签中用this.state.value替换this.props.value
  • () => this.setState({value: 'X'})替换() => alert()
  • classNameonClick各占一行以便更好的阅读

改变以后<button>标签应该是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}

render() {
return (
<button
className="square"
onClick={() => this.setState({value: 'X'})}
>
{this.state.value}
</button>
);
}
}

通过调用Square中的onClick方法执行this.setState方法,我们高速React去重新渲染这个组件。更新之后Square的this.state.value将变成X,这样,如果我们点击任何一个Square,将会在其中展示一个'X'

当我们在组件中调用setState方法,React也将自动更新其中的子组件。

看此部分完整代码

开发工具(Developer Tools)

这里介绍的是React在ChromeFirefox的插件,先不翻译

完成小游戏项目

有了这个游戏的基础框架,要完成这个游戏,我们需要去瓤“X”和“O”能够在游戏面板中交替下棋,并且需要一个方法去决定谁是赢家。

State

目前,每个Square组件都有一个游戏的State,为了一决定谁是赢家,我们将在同一个地方构建每一个Square的value。

我们可以认为Board仅仅是询问了每一个Square的state,尽管在React中这种方法可以实现,我们也不将使用,因为它这样代码将相对难以理解,更容易产生bug并且难遇维护。储存游戏状态更好的方法是在父组件Board中储存,而不是在每一个Square中储存,Board组件通过传递props能够告诉每一个Square该去显示什么,就像我们之前用props传递数据一样

去采集来自数个子组件的数据,或者让两个子组件相互通信交流,你需要去共享父组件的state,父组件可以用props向下传递state;这能让子组件随着父组件相互同步。

当React组件被复用时,将state拿到父组件中在React中是一个常见的方法,让我们把握这个机会试一下!
我们在Board增加一个构造方法,并且初始化state,包含含有9个null的数组,这9个null分别对应9个方块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}

renderSquare(i) {
return <Square value={i} />;
}

render() {
const status = 'Next player: X';

return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}

当我们以后填满棋盘,我们将看到类似:

1
2
3
4
5
[
'O', null, 'X',
'X', 'X', 'O',
'O', null, null,
]

Board组件中的renderSquare方法现在是这样的;

1
2
3
renderSquare(i) {
return <Square value={i} />;
}

之前我们传递0-8九个数字给每一个方块,不同于之前的步骤,我们用Square中state中的值“X”代替数字,这是我们数字不能显示的原因。

我们现在将再次使用props传递机制。我们现在修改Board去指导每个独立的方块。我们已经定义了squares数组,然后我们将修改Board的renderSquare的方法去拿到它:

1
2
3
renderSquare(i) {
return <Square value={this.state.squares[i]} />;
}

看此部分完整代码

每个方块现在将接收到一个valueprop,它将是'X','O',null其中一个值。

下一步,我们需要去对方块点击后发生的事件做改变,Board组件现在决定那写方块被填满,我们需要去用一个方法让Square组件能够更新Board的state,既然state对每个组件来说是私有的。我们不能直接在square组件中修改Board组件的state。

为了维持state的私有性,我们需要从Board向下传递一个方法,这个方法将被调用。下面我们改变Board组件中的renderSquare方法:

1
2
3
4
5
6
7
8
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}

Note
为了更好的可读性,我们把元素分行,并且我们加了一个圆括号解决能让return之后架上分号,并且结束代码。

下载我们从Board传了两个props给Square:valueonClick. 这个onClick是一个Square组件被点击时可以调用的一个方法,我们对Square做如下的工作:

  • 在Square的render方法中,用this.props.value替换this.state.value
  • 在Square的render方法中,用this.props.onClick()替换this.setState()
  • 删除Square的constructor,因为它不再用来跟踪游戏的状态了

    改变之后的Square组件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
      class Square extends React.Component {
    render() {
    return (
    <button
    className="square"
    onClick={() => this.props.onClick()}
    >
    {this.props.value}
    </button>
    );
    }
    }

当一个方块被点击,Board传过来的onClick方法将被调用,下面来看看为什么:

  1. <button>组件中的onClick属性通知React创建了一个点击事件的监听事件
  2. 当按钮被点击,React将调用在Square组件的render()方法中的onClick事件处理器
  3. 这个事件处理器再调用this.props.onClick(),这个Square的onClick属性具体的内容在Board组件中
  4. 既然Board组件传递onClick={() => this.handleClick(i)}给Square,那个这个Square当被点击时调用的是this.handleClick(i)
  5. 我们还没定义handleClick()方法,那么我们就开始干咯。

Note
DOM下的<button>元素的onClick内置的组件. 对于像Square一样的自定义标签,命名是取决于你的, 我们完全可以将Square的onClick属性或者Board的handleClick方法命名成别的名字. 然而在React中为一些代表事件的属性使用on[Event]这样的命名格式是一种约定而成的习惯, handle[Event]作为处理方法的名称也是这个道理。

当我们试着去点击一个方块,应该会报一个error,因为我们还没定义handleClick方法,我们现在开始在Board class中写handleClick方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}

handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = 'X';
this.setState({squares: squares});
}

renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}

render() {
const status = 'Next player: X';

return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}

看此部分完整代码

这样之后,我们又能够点击且填满方块了,但是现在它们的状态state被储存在Board组件而不是每个独立的Square组件中。当我们Boare的state改变,Square组件将自动重新渲染,让所有方块的状态的状态state都放在Board组件中将让我们之后的开发中可以决定谁是赢家。

既然方块组件(Square components)不再有state,它仅仅做的是接收来自棋盘组件(Board component)的values值并且当被点击时通知棋盘组件。在React中,方块组件现在叫做被控制组件,棋盘完全控制着它们。

注意在handleClick中,我们调用了.slice()来创建一个squares数组的copy,然后修改它而不是直接修改之前就存在的,下面我们将解释我们为什么要这么做。

为什么不变性如此重要(Why Immutability Is Important)

在之前的代码中,我们推荐使用.slice()去创建squares数组的copy,然后修改它而不是直接修改之前就存在的。我们现在讨论为何不改变之前的数组,为何踏实重要的。

我们知道,改变数据有两个方法一个是直接改变数据的值,还有就是修改一个数据拷贝的副本。

直接改变数据

1
2
3
 var player = {score: 1, name: 'Jeff'};
player.score = 2;
// Now player is {score: 2, name: 'Jeff'}

间接得到改变的数据

1
2
3
4
5
6
7
var player = {score: 1, name: 'Jeff'};

var newPlayer = Object.assign({}, player, {score: 2});
// Now player is unchanged, but newPlayer is {score: 2, name: 'Jeff'}

// Or if you are using object spread syntax proposal, you can write:
// var newPlayer = {...player, score: 2};

复杂的功能变简单

不可变性是复杂的功能实现起来变得更简单,在之后的教程中,我们将实现一个time travel的功能,可以允许我们玩游戏的过程中回顾之前的步骤,还有悔棋,这是这个游戏的普通需求,避免去直接改变数据让我们能够完好无损回到游戏游戏的历史界面。

观察变动

数据直接改变后想去观察是困难的,这种观察房需要可变的对象与之前他本身的的各种copy版本进行比较

观察改变在不变的对象的情况下是更容易的,如果当前的不变对象与之前的是不同的,那么说明已经改变。

决定何时重新加载

不可变的主要益处设市能够帮助我们构建一个pure components(纯净的组件??),不便数据可以是我们易于发现改变何时发生并且帮助我们决定何时重新加载。

你可以学习更多关于shouldComponentUpdate()并且通过阅读Optimizing Performance学习如何构建pure components

功能组件(Functional Components)

我们现在让Square变成一个functional component.

在react中,功能组件(functional component)是一个写组件的简单方式,其中仅包含一个render方法,并且没有自己的state,代替定义一个继承自React.Component的类,我们可以写一个function,props作为参数,并且返回我们想要渲染的内容,比起组件类功能组件是单调的,许多简单的组件可以写成这样的形式:

1
2
3
4
5
6
7
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}

我们将其中的两处this.props替换成this.props

看此部分完整代码

Note
当我们将Square修改成功能组件时,我们也将onClick={() => this.props.onClick()}变为更简短的onClick={props.onClick}(不要缺少两边的括号),在类中我们需要用箭头函数去使用正确的this的值,但是在功能组件中我们无需担心this

轮流下棋

我们现在需要去改善游戏中一个明显的bug:“O”不能在标记在棋盘中

我们将默认第一步下“X”,我们可以在Board组件中修改这个默认值:

1
2
3
4
5
6
7
8
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}

每次一个玩家移动完,xIsNext(boolean)将被迅速取反曲决定下一步的玩家,并且游戏的state将被保存,我们修改Board的handleClick方法,加入xIsNext

1
2
3
4
5
6
7
8
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}

通过这个改变,“X”和“O”可轮流切换,让我们也改变在棋盘状态文本status去展示下一步的玩家:

1
2
3
4
5
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

return (
// the rest has not changed

做了这个改变之后,你的棋盘组件应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}

handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}

renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}

render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}

宣布赢家

既然我们已经展示了下一步玩家是哪一位,我们也应当展示游戏何时分出胜负并且结束游戏,我们可以通过增加下面这个帮助函数放在文件尾部实现这个功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}

我们将在Board的render中调用这个calculateWinner(squares)函数去何时游戏时候分出胜负,如果有玩家胜出了我们将展示出来,例如“Winner: X”,我们将在Board的render方法中替换status通过下面的代码:

1
2
3
4
5
6
7
8
9
10
11
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}

return (
// the rest has not changed

我们现在可以修改Board的handleClick函数,如果有玩家赢了游戏,去提前return去不响应点击事件。

1
2
3
4
5
6
7
8
9
10
11
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}

[看此部分完整代码]看此部分完整代码

祝贺你!你现在已经差不多完成了这个游戏了,并且你也已经学习到了react的基础知识,到这里,你可能已经是一个真正的赢家了!!!