那年夏天

React

2017-10-26  本文已影响74人  坚持到底v2

官方文档

https://react.docschina.org/docs/hello-world.html


一. 安装

1. 创建一个新的React App

如果你正在学习 React 或者创建一个新的SPA, 推荐使用 Create React App.
create-react-app 需要 Node >= 6 and npm >= 5.2
create-react-app 使用 Babel 和 webpack, 但是你可以不用知道其细节.

npx create-react-app my-app

你也可以从头构建工具链:
使用 yarnnpm 作为包管理器
使用 webpack 或其他构建工具
使用 babel 或其他编译器.


二. 主要概念

1. JSX介绍

JSX是语法糖

本质上来讲,JSX 只是为 React.createElement(component, props, ...children) 方法提供的语法糖。
比如下面的代码:

<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>

// 编译为:
React.createElement(
  MyButton, // 元素名
  {color: 'blue', shadowSize: 2}, // 属性
  'Click Me' // 子代
)

// 如果没有子代,你还可以使用自闭合标签,比如:
<div className="sidebar" />

// 编译为:
React.createElement(
  'div',
  {className: 'sidebar'},
  null
)

大写开头的 JSX 标签表示一个 React 组件

大写开头的 JSX 标签表示一个 React 组件。
这些标签将会被编译为同名变量并被引用,所以如果你使用了 <Foo /> 表达式,则必须在作用域中先声明 Foo 变量

必须首先声明 React 变量

由于 JSX 编译后会调用 React.createElement 方法,所以在你的 JSX 代码中必须首先声明 React 变量。

扩展属性

如果你已经有了个 props 对象,并且想在 JSX 中传递它,你可以使用 ... 作为扩展操作符来传递整个属性对象。下面两个组件是等效的:

function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

子代

在包含开始和结束标签的 JSX 表达式中,标记之间的内容作为特殊的参数传递:props.children 。有几种不同的方法来传递子代:

字符串常量
你可以在开始和结束标签之间放入一个字符串,则 props.children 就是那个字符串。这对于许多内置 HTML 元素很有用。例如:

<MyComponent>Hello world!</MyComponent>

这是有效的 JSX,并且 MyComponent 的 props.children 值将会直接是 "hello world!"。
因为 HTML 未转义,所以你可以像写 HTML 一样写 JSX.

JSX
你可以通过子代嵌入更多的 JSX 元素,这对于嵌套显示组件非常有用:

<MyContainer>
  <MyFirstComponent />
  <MySecondComponent />
</MyContainer>

布尔值、Null 和 Undefined 被忽略
false、null、undefined 和 true 都是有效的子代,但它们不会直接被渲染。下面的表达式是等价的:

<div />

<div></div>

<div>{false}</div>

<div>{null}</div>

<div>{undefined}</div>

<div>{true}</div>

子代遍历

React.Children.map(this.props.children, function (child) {
    return <li>{child}</li>;
})

注意, this.props.children 的值有4种可能:

所以,处理 this.props.children 的时候要小心。

React 提供一个工具方法 React.Children 来处理 this.props.children
我们可以用 React.Children.map 来遍历子节点,而不用担心 this.props.children 的数据类型是 undefined 还是 object

2. 元素渲染

// 创建一个元素
const element = <h1>Hello, world</h1>;
// 渲染
ReactDOM.render(element, document.getElementById('root'));

3. 组件 & Props

// 使用函数定义组件
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// 使用类定义组件
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

// 创建元素
const element = <Welcome name="Sara" />;

组件的属性是只读的!!!
无论是使用函数或是类来声明一个组件,它决不能修改它自己的props
但是如果组件的属性是一个对象,我们没有更改其属性指向另一个对象,但是更改了对象的内容时,渲染不会刷新!!
所以所有的React组件必须像纯函数那样使用它们的props
后面再详细介绍

4. State & 生命周期

一个例子:


// 使用类定义组件
class Clock extends React.Component {
    // 构造器
    constructor(props) {
        super(props);
        this.state = {date: new Date()};
    }

    // 生命周期钩子函数
    componentDidMount() {
        this.timerID = setInterval(
            () => this.tick(),
            1000
        );
    }

    // 生命周期钩子函数
    componentWillUnmount() {
        clearInterval(this.timerID);
    }

    tick() {
        this.setState({
            date: new Date()
        });
    }

    render() {
        return (
            <div>
                <h1>Hello, world!</h1>
                <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
            </div>
        );
    }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

Q: 我应该在组件的哪个生命周期发送 AJAX 请求?
A: 你应该在 componentDidMount 生命周期方法内发送 AJAX 请求数据。

 这样你才能够在请求的数据到达时使用 setState 更新你的组件。

需要注意的是,如果组件在 AJAX 请求完成之前被卸载了,那么你会在浏览器的控制面板上看到一条警告:cannot read property 'setState' of undefined
如果这对你来说是个问题的话,你可以追踪未完成的 AJAX 请求并在 componentWillUnmount 生命周期方法内将它们取消。

5. 事件处理

例子1:使用函数定义组件时的写法

function ActionLink() {
  function handleClick(e) {
    // 在 React 中另一个不同是你不能使用返回 false 的方式阻止默认行为。
    // 你必须明确的使用 preventDefault
    e.preventDefault();

    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}

ReactDOM.render(
  <ActionLink />,
  document.getElementById('root')
);

例子2: 使用类定义组件时的写法

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // 这个binding是必须的,
    // 否则在回调中使用this将指向 undefined 
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

ReactDOM.render(
  <Toggle />,
  document.getElementById('root')
);

如果使用 bind 让你很烦,这里有两种方式可以解决, 因为不推荐,这里就不写了,待续

例子3: 向事件处理程序传递参数

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
// 推荐第二种
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。
值得注意的是,通过 bind 方式向监听函数传参,在类组件中定义的监听函数,事件对象 e 要排在所传递参数的后面,例如:

class Popper extends React.Component{
    constructor(){
        super();
        this.state = {name:'Hello world!'};
    }
    
    //事件对象e要放在最后
    preventPop(name, e){    
        e.preventDefault();
        alert(name);
    }
    
    render(){
        return (
            <div>
                <p>hello</p>
                // 通过bind方式传递参数
                <a href="https://reactjs.org" onClick={this.preventPop.bind(this,this.state.name)}>Click</a>
            </div>
        );
    }
}

ReactDOM.render(
  <Popper />,
  document.getElementById('root')
);

6. 条件渲染

例子1: if

function UserGreeting(props) {
  return <h1>Welcome back!</h1>;
}

function GuestGreeting(props) {
  return <h1>Please sign up.</h1>;
}

function Greeting(props) {
  const isLoggedIn = props.isLoggedIn;
  if (isLoggedIn) {
    return <UserGreeting />;
  }
  return <GuestGreeting />;
}

ReactDOM.render(
  // Try changing to isLoggedIn={true}:
  <Greeting isLoggedIn={false} />,
  document.getElementById('root')
);

还可以借助 属性存储函数进行绑定,待续

例子2: 与运算符 &&

function Mailbox(props) {
  const unreadMessages = props.unreadMessages;
  return (
    <div>
      <h1>Hello!</h1>
      {unreadMessages.length > 0 &&
        <h2>
          You have {unreadMessages.length} unread messages.
        </h2>
      }
    </div>
  );
}

const messages = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
  <Mailbox unreadMessages={messages} />,
  document.getElementById('root')
);

例子3: 三目运算符

function Greeting(props) {
    const isLoggedIn =  props.isLoggedIn;
    return (
        <div>
            The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
        </div>
    );
    
}

ReactDOM.render(
  // Try changing to isLoggedIn={true}:
  <Greeting isLoggedIn={false} />,
  document.getElementById('root')
);

例子4: 阻止组件渲染
让 render 方法返回 null 而不是它的渲染结果即可实现。

7. 列表 & Keys

8. 表单

9. state提升

使用 react 经常会遇到几个组件需要共用状态数据的情况。
这种情况下,我们最好将这部分共享的状态提升至他们最近的父组件当中进行管理。

经验:
在React应用中,对应任何可变数据理应只有一个单一“数据源”。
通常,状态都是首先添加在需要渲染数据的组件中。
此时,如果另一个组件也需要这些数据,你可以将数据提升至离它们最近的父组件中。
你应该在应用中保持 自上而下的数据流,而不是尝试在不同组件中同步状态。

如果某些数据可以由props或者state提供,那么它很有可能不应该在state中出现。
举个例子,我们仅仅保存最新的编辑过的temperature和scale值,而不是同时保存 celsiusValue 和 fahrenheitValue 。
另一个输入框中的值总是可以在 render() 函数中由这些保存的数据计算出来。
这样我们可以根据同一个用户输入精准计算出两个需要使用的数据。

例子:

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>水会烧开</p>;
  }
  return <p>水不会烧开</p>;
}

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.props.onTemperatureChange(e.target.value);
  }

  render() {
    const temperature = this.props.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>在{scaleNames[scale]}:中输入温度数值</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

function toCelsius(fahrenheit) {
  return (fahrenheit - 32) * 5 / 9;
}

function toFahrenheit(celsius) {
  return (celsius * 9 / 5) + 32;
}

function tryConvert(temperature, convert) {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return '';
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}


class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
    this.state = {temperature: '', scale: 'c'};
  }

  handleCelsiusChange(temperature) {
    this.setState({scale: 'c', temperature});
  }

  handleFahrenheitChange(temperature) {
    this.setState({scale: 'f', temperature});
  }

  render() {
    const scale = this.state.scale;
    const temperature = this.state.temperature;
    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
    const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;

    return (
      <div>
        <TemperatureInput
          scale="c"
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange} />

        <TemperatureInput
          scale="f"
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange} />

        <BoilingVerdict
          celsius={parseFloat(celsius)} />

      </div>
    );
  }
}

ReactDOM.render(
  <Calculator />,
  document.getElementById('root')
);

10. 组合 vs 继承

10.1 包含关系

一些组件不能提前知道它们的子组件是什么。这对于 Sidebar 或 Dialog 这类通用容器尤其常见。
我们建议这些组件使用 children 属性将子元素直接传递到输出。

例子:

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

ReactDOM.render(
  <WelcomeDialog />,
  document.getElementById('root')
);

10.2 特殊实例

有时我们认为组件是其他组件的特殊实例。
例如,我们会说 WelcomeDialog 是 Dialog 的特殊实例。

在 React 中,这也是通过组合来实现的,通过配置属性用较特殊的组件来渲染较通用的组件。

function Dialog(props) {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
    </FancyBorder>
  );
}

function WelcomeDialog() {
  return (
    <Dialog
      title="Welcome"
      message="Thank you for visiting our spacecraft!" />

  );
}

10.3 那么继承呢?

在 Facebook 网站上,我们的 React 使用了数以千计的组件,然而却还未发现任何需要推荐你使用继承的情况。

属性和组合为你提供了以清晰和安全的方式自定义组件的样式和行为所需的所有灵活性。
请记住,组件可以接受任意元素,包括基本数据类型、React 元素或函数。

如果要在组件之间复用 UI 无关的功能,我们建议将其提取到单独的 JavaScript 模块中。
这样可以在不对组件进行扩展的前提下导入并使用该函数、对象或类

但是我认为继承还是有用的, 例如继承 React.Component 或其他组件, 形成团队中使用的有相对固定规范的 基础类

11. React 理念


三. 高级指引

1. 使用 PropTypes 进行类型检查

注意: React.PropTypes 自 React v15.5 起已弃用。请使用 prop-types 库代替。
也就是说目前以及被废弃

随着应用日渐庞大,你可以通过类型检查捕获大量错误。
对于某些应用来说,你还可以使用 FlowTypeScript 这样的 JavsScript 扩展来对整个应用程序进行类型检查。

https://www.npmjs.com/package/prop-types

2. 静态类型检查

FlowTypeScript 这样的静态类型检查器可以在运行代码之前识别某些类型的问题。
他们还可以通过添加自动完成功能来改善开发人员的工作流程。
出于这个原因,对于更大的代码库我们建议使用 Flow 或者 TypeScript 来替代 PropTypes。

https://react.docschina.org/docs/static-type-checking.html

3. Refs & DOM

何时使用 Refs

下面是几个适合使用 refs 的情况:

如果可以通过声明式实现,则尽量避免使用 refs。
例如,不要在 Dialog 组件上直接暴露 open() 和 close() 方法,最好传递 isOpen 属性。

不要过度使用 Refs

https://react.docschina.org/docs/refs-and-the-dom.html

4. 非受控组件

在大多数情况下,我们推荐使用 受控组件 来实现表单。
在受控组件中,表单数据由 React 组件处理。
如果让表单数据由 DOM 处理时,替代方案为使用非受控组件。

要编写一个非受控组件,而非为每个状态更新编写事件处理程序,你可以 使用 ref 从 DOM 获取表单值。

https://react.docschina.org/docs/uncontrolled-components.html

5. 性能优化

https://react.docschina.org/docs/optimizing-performance.html

6. 不使用ES6

https://react.docschina.org/docs/react-without-es6.html

7. 不使用 JSX

https://react.docschina.org/docs/react-without-jsx.html

8. 协调(Reconciliation)

React提供了一组声明式API以让你不必关心每次更新的变化。
这使得应用的编写容易了很多,但在React中如何实现并不是很清晰。
这篇文章解释了React对比算法的选择以让组件更新可预测并使得高性能应用足够快。

https://react.docschina.org/docs/reconciliation.html

9. Context

Context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性。

https://react.docschina.org/docs/context.html

10. Fragments

为什么使用 Fragments

React 中一个常见模式是为一个组件返回多个元素。
Fragments 可以让你聚合一个子元素列表,并且不在DOM中增加额外节点。

以这个示例的 React 片段为例:

class Table extends React.Component {
  render() {
    return (
      <table>
        <tr>
          <Columns />
        </tr>
      </table>
    );
  }
}

为了渲染有效的 HTML , <Columns /> 需要返回多个 <td> 元素。
如果一个父 div 在 <Columns /> 的 render() 函数里面使用,那么最终的 HTML 将是无效的。

class Columns extends React.Component {
  render() {
    return (
      <div>
        <td>Hello</td>
        <td>World</td>
      </div>
    );
  }
}

在 <Table /> 组件中的输出结果如下:

<table>
  <tr>
    <div>
      <td>Hello</td>
      <td>World</td>
    </div>
  </tr>
</table>

所以,我们介绍 Fragments。

class Columns extends React.Component {
  render() {
    return (
      <>
        <td>Hello</td>
        <td>World</td>
      </>
    );
  }
}

// 另一种使用片段的方式是使用 React.Fragment 组件,React.Fragment 组件可以在 React 对象上使用。  
// 这可能是必要的,如果你的工具还不支持 JSX 片段。  
// 注意在 React 中, <></> 是 <React.Fragment/> 的语法糖。

class Columns extends React.Component {
  render() {
    return (
      <React.Fragment>
        <td>Hello</td>
        <td>World</td>
      </React.Fragment>
    );
  }
}

在正确的 <Table /> 组件中,这个结果输出如下:

<table>
  <tr>
    <td>Hello</td>
    <td>World</td>
  </tr>
</table>

https://react.docschina.org/docs/fragments.html

11. Portals 在父组件外部渲染子节点

Portals 提供了一种很好的将子节点渲染到父组件以外的 DOM 节点的方式。

ReactDOM.createPortal(child, container)

第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或碎片。
第二个参数(container)则是一个 DOM 元素。

https://react.docschina.org/docs/portals.html

12. Error Boundaries 错误边界

部分 UI 的异常不应该破坏了整个应用。
为了解决 React 用户的这一问题,React 16 引入了一种称为 “错误边界” 的新概念。

错误边界是用于捕获其子组件树 JavaScript 异常,记录错误并展示一个回退的 UI 的 React 组件,而不是整个组件树的异常。
错误组件在渲染期间,生命周期方法内,以及整个组件树构造函数内捕获错误。

13. Web Components(web 组件)

你可以随意地在Web组件里使用React,或者在React里使用Web组件,或都有。
https://react.docschina.org/docs/web-components.html

14. 高阶组件( High-Order Components == HOCs )

高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件

https://react.docschina.org/docs/higher-order-components.html

15. Forwarding Refs(传递Refs)

Ref forwarding 是组件将 ref 传递给其后代组件的一种机制.
这个机制对高阶组件尤其有用.

https://react.docschina.org/docs/forwarding-refs.html

16. Render Props ( 组件的属性为函数 )

术语 “render prop” 是指一种在 React 组件之间使用一个值为函数的 prop 在 React 组件间共享代码的简单技术。

17. 与第三方库协同

我们可以在任何网页应用中使用 React。
不仅可以把 React 添加到其他应用里,
而且只要稍作改动,我们也可以把其他应用添加到 React 项目里。

本文将着重介绍如何将 React 与 jQuery 以及 Backbone 结合使用。
当然,类似的思路同样可以应用与其他场景。

与 DOM 节点操作类插件结合

对于 React 之外的 DOM 节点操作,React 是不会去处理的,因为 React 内部有自己的渲染逻辑。
当相同的 DOM 节点被外部的代码改变时,React 就会很迷茫,并不知道发生了什么。

但这也不意味着 React 无法与其他操作 DOM 节点的库一起使用,你只要清楚他们分别在做什么就可以了。

最简单的方式就是阻止 React 更新外部在操作的节点,那么你可以通过生成一个 React 根本不会去更新的元素来实现,比如空的 <div />

以封装一个通用的 jQuery 插件为例

为了解释得更清楚,我们先来封装一个通用的 jQuery 插件吧。

在这里,我们给 DOM 的根节点元素加了一个 ref
componentDidMount 中,我们会调用这个 ref ,并把它传给 jQuery 插件

为了防止 React 在 DOM 加载后修改节点,我们先要在 render() 中返回一个空的 <div />
这个空的 <div /> 既没有属性也没有子元素,这样一来,React 就不会更新它了。
那么,我们封装的 jQuery 插件就可以随意地更新这个节点。

class SomePlugin extends React.Component {
  componentDidMount() {
    this.$el = $(this.el);
    this.$el.somePlugin();
  }

  componentWillUnmount() {
    this.$el.somePlugin('destroy');
  }

  render() {
    return <div ref={el => this.el = el} />;
  }
}

值得注意的是,我们既调用了 componentDidMount 也调用了 componentWillUnmount 生命周期函数。
由于很多 jQuery 的插件都会在 DOM 上挂载事件监听器,因此我们必须要在 componentWillUnmount 的时候把这个监听器删掉。
如果某个插件没有提供“删除监听器”这类的方法,那你很可能需要自己写一个。
为了防止内存泄漏,请务必在生命周期函数中移除插件挂载的事件监听器。

https://react.docschina.org/docs/integrating-with-other-libraries.html

18. 可访问性

https://react.docschina.org/docs/accessibility.html

19. Code-Splitting(代码分隔)

打包非常棒,但随着你的应用增长,你的代码包也将随之增长。
尤其是如果你包含了体积大的第三方库。
你需要关注你代码包中所包含的代码以避免体积过大而使得加载时间过长。

为了避免清理大体积的代码包,在一开始就解决该问题并开始对代码包进行分割则十分不错。
代码分割是由如 webpack 和 Browserify(通过 factor-bundle)等打包器支持的一项能够创建多个包并在运行时动态加载的特性。

代码分割你的应用能够帮助你“懒加载”当前用户所需要的内容,能够显著地提高你的应用性能。
尽管你不用减少你的应用中过多的代码体积,你仍然能够避免加载用户永远不需要的代码,并在初始化时候减少所需加载的代码量。

https://react.docschina.org/docs/code-splitting.html

20. Strict Mode(严格模式)

StrictMode是一个用以标记出应用中潜在问题的工具。
就像Fragment,StrictMode不会渲染任何真实的UI。
它为其后代元素触发额外的检查和警告。

你可以在应用的任何地方启用严格模式。
例如下面的例子 不会对组件Header、Footer进行strict mode检查。
然而ComponentOne、ComponentTwo以及它们所有的后代将被检查:

import React from 'react';

function ExampleApplication() {
  return (
    <div>
      <Header />
      <React.StrictMode>
        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>
      <Footer />
    </div>
  );
}

StrictMode目前有助于:

注意: 严格模式检查只在开发模式下运行,不会与生产模式冲突。

https://react.docschina.org/docs/strict-mode.html


四. API 参考

https://react.docschina.org/docs/react-api.html

上一篇下一篇

猜你喜欢

热点阅读