react学习笔记(上)

这里有一份简洁的前端知识体系等待你查收,看看吧,会有惊喜哦~若是以为不错,麻烦star哈~css


前言

react 是数据驱动的框架,也是目前前端最火的框架之一,学习react,咱们照旧从应用维度跟设计维度进行学习。html


应用维度


问题

从技术的应用维度看,首先考虑的是要解决什么问题,这是技术产生的缘由。问题这层,用来回答“干什么用”。前端


react 的诞生实际上是要解决两个问题。UI细节问题问题 和 数据模型的问题。node

UI细节问题问题react

传统UI操做关注太多细节,jQuery虽然能够给咱们提供了便捷的API,以及良好的浏览器兼容,但开发人员仍是要手动去操做DOM,关注太多细节,不只下降了开发效率,还容易引入BUG。webpack

react以数据为中心,数据驱动视图,而不直接操做dom,也就是只负责描述界面应该显示成什么样子,而不关心实现细节。git

数据模型的问题github

在react以前,前端管理数据的模型是MVC架构。传统的MVC架构难以扩展和维护,当应用程序出现问题,很难知道是model仍是view出现问题。web



react采用的是单向数据流,能够很好的避免相似的问题。数据库


技术规范

技术被研发出来,人们怎么用它才能解决问题呢?这就要看技术规范,能够理解为技术使用说明书。技术规范,回答“怎么用”的问题,反映你对该技术使用方法的理解深度。


React 的基本原则

要真正理解 React,开发者必需要明白这几点:

  • React 界面彻底由数据驱动;
  • React 中一切都是组件;
  • props 是 React 组件之间通信的基本方式。

下面逐一分析。


笔者认为:学习框架,关键是要关注框架自己解决了什么问题,以及如何解决,这些才是框架最核心的部分,若是胡子眉毛一把抓,很容易就迷失在知识的海洋中。

细心的你确定会发现,react的三个基本原则,正是解决上文提到的两大传统的良方。

针对UI细节问题,react的解决方案就是:数据驱动与组件化;

针对数据模型,react的解决方案是单向数据流,而props为单向数据流提供了支持。

学习react的过程当中,牢记两大传统问题以及react开出的解决方案,有利于你造成系统思惟,强化你的react的理解。


界面彻底由数据驱动

React 的哲学,简单说来能够用下面这条公式来表示:

UI = f(data)
复制代码

等号左边的 UI 表明最终画出来的界面;等号右边的 f 是一个函数,也就是咱们写的 React 相关代码;data 就是数据,在 React 中,data 能够是 state 或者 props。

UI 就是把 data 做为参数传递给 f 运算出来的结果。这个公式的含义就是,若是要渲染界面,不要直接去操纵 DOM 元素,而是修改数据,由数据去驱动 React 来修改界面

咱们开发者要作的,就是设计出合理的数据模型,让咱们的代码彻底根据数据来描述界面应该画成什么样子,而没必要纠结如何去操做浏览器中的 DOM 树结构。

这样一种程序结构,是声明式编程(Declarative Programming)的方式,代码结构会更加容易理解和维护。


组件:React 世界的一等公民

在 React 中一切皆为组件。这是由于:

  • 用户界面就是组件;
  • 组件能够嵌套包装组成复杂功能;
  • 组件能够用来实现反作用。

第一点用户界面就是组件,很好理解,咱们须要一个按钮,就能够实现一个Button组件,在 React 中,一个组件能够是一个类,也能够是一个函数,这取决于这个组件是否有本身的状态。

第二点,组件能够嵌套包装组成复杂功能。现实中的应用是很复杂的,React 中的组件能够重复嵌套,就是为了支持现实中的用户界面须要。

第三点,组件能够用来实现反作用。并非说组件必需要在界面画一些东西,一个组件能够什么都不画,或者把画界面的事情交给其余组件去作,本身作一些和界面无关的事情,好比获取数据。


组件之间的语言:props

父组件想要传递数据给子组件,能够经过 props。

一样,子组件想要传递数据给父组件,可让父组件传递一个函数类型的 props 进来,当子组件要传递数据给父组件时,调用这个函数类型 props,就把信息传递给了父组件。

若是两个彻底没有关系的组件之间有话说,状况就复杂了一点,好比下图中,两个橙色组件之间若是有话说,就无法直接经过 props 来传递信息。



一个比较土的方法,就是经过 props 之间的逐步传递,来把这两个组件关联起来。若是之间跨越两三层的关系,这种方法还凑合,可是,若是这两个组件隔了十几层,或者说所处位置多变,那让 props 跨越千山万水来相会,实在是得不偿失。

另外一个简单的方式,就是创建一个全局的对象,两个组件把想要说的话都挂在这个全局对象上。这种方法固然简单可行,可是,咱们都知道全局变量的危害罄竹难书,若是不想未来被难以维护的代码折磨,咱们最好对这种方法敬而远之。

通常,业界对于这种场景,每每会采用第三方数据管理工具来解决,好比 Redux 和 Mobx 。

其实,不依赖于第三方工具,React 也提供了本身的跨组件通信方式,这种方式叫 Context,后面会介绍。

小结:

  • 父组件向子组件通讯:props
  • 子组件向父组件通讯
    • 利用回调函数
    • 利用自定义事件机制
  • 跨级组件通讯:context
  • 没有嵌套关系的组件通讯:自定义事件机制

组件设计

咱们平常建立组件的通常步骤,是这样的:

  1. 建立静态 UI
  2. 考虑组件的状态组成:状态(state) 及 状态的改变(effect、reducer)
  3. 考虑组件的交互方式:状态的触发(dispatch)

若是要设计出更优雅的组件,咱们还要了解组件设计原则。

React 组件设计原则,简单说来,就是高内聚、低耦合。

低耦合

就是要减小组件之间的耦合性,让系统易于理解、易于维护。

因此,建立组件的原则,概括起来有两点:

  1. 什么时候建立组件:单一职责原则
    • 每一个组件只作一件事
    • 若是组件变得复杂,那么应该拆分红小组件
  2. 数据状态管理:DRY原则
    • 能计算获得的状态就不要单独存储
    • 组件尽可能无状态,所需数据经过 props 获取

更具体一点,在设计 React 组件时,要注意如下事项:

  • 保持接口小,props 数量要少;
  • 根据数据边界来划分组件,充分利用组合;
  • 把 state 往上层组件提取,让下层组件只须要实现为纯函数。

高内聚

传统的网页应用分为三层,分别是用 HTML 实现的“内容”,用 CSS 实现的“样式”,还有用 JS 实现的“动态行为”。

HTML、CSS 和 JS 被分开管理,致使的问题是要修改一个功能,须要至少修改三个文件,这就违背了高内聚的原则。

在 React 中,当你要修改一个功能的内容和行为时,在一个文件中就能完成,这样就知足了高内聚的要求。

在react中处理CSS,官方没有一个统一的标准,请看后面样式处理的章节。

组件设计的内容远不止这些。社区有很是多的最佳实践,请看组件设计模式


状态管理·组件状态

在前面的章节中,咱们反复声明过 React 其实就是这样一个公式:

UI = f(data)
复制代码

f 的参数 data,除了 props,就是 state。props 是组件外传递进来的数据,state 表明的就是 React 组件的内部状态。

为何要了解 React 组件自身状态管理

虽然有 Redux 和 Mobx 这样的状态管理工具,不过,咱们首先不要管这些第三方工具,先从了解 React 组件自身的管理开始。

为何呢?

第一个缘由,由于 React 组件自身的状态管理是基础,其余第三方工具都是在这个基础上构筑的,连基础都不了解,没法真正理解第三方工具。

另外一个重要缘由,对于不少应用场景,React 组件自身的状态管理就足够解决问题,犯不上动用 Redux 和 MobX 这样的大杀器,简单问题简单处理,可让代码更容易维护。

组件自身状态 state

什么数据放在 state 中

判断一个数据应该放在哪里,用下面的原则:

  • 若是数据由外部传入,放在 props 中;
  • 若是是组件内部状态,是否这个状态更改应该马上引起一次组件从新渲染?若是是,放在 state 中;不是,放在成员变量中。
修改 state 的正确方式

不能直接修改 state 对象,必须使用 this.setState。

由于使用 setState 函数,那不光修改 state,还能引起组件的从新渲染。

state 改变引起从新渲染的时机

React 为了性能考虑,不会每次 setState 都引起从新渲染。

this.setState({count: 1});
this.setState({caption: 'foo'});
this.setState({count: 2});
复制代码

连续的同步调用 setState,第三次还覆盖了第一次调用的效果,可是效果只至关于调用了下面这样一次:

this.setState({count: 2, caption: 'foo'});
复制代码

每一个 setState 都引起一次从新渲染,实在太浪费了。

React 很是巧妙地用任务队列解决了这个问题,能够理解为每次 setState 函数调用都会往 React 的任务队列里放一个任务,屡次 setState 调用天然会往队列里放多个任务。React 会选择时机去批量处理队列里执行任务,当批量处理开始时,React 会合并多个 setState 的操做,好比上面的三个 setState 就被合并为只更新 state 一次,也只引起一次从新渲染。

由于这个任务队列的存在,React 并不会同步更新 state,因此,在 React 中,setState 也不保证同步更新 state 中的数据。

state 不会被同步修改

简单说来,调用 setState 以后的下一行代码,读取 this.state 并非修改以后的结果。

console.log(this.state.count);// 修改以前this.state.count为0
this.setState({count: 1})
console.log(this.state.count);// 在这里this.state.count依然为0
复制代码

这是由于React 的任务队列机制。setState 只是给任务队列里增长了一个修改 this.state 的任务,这个任务并无当即执行,因此 this.state 并不会马上改变。

但也有例外。由 React 的生命周期函数或者事件处理函数以外引发的 setState ,就能够同步更新 state

看下面的代码,结果可能会出乎你的所料:

setTimeout(() => {
  this.setState({count: 2}); //这会马上引起从新渲染
  console.log(this.state.count); //这里读取的count就是2
}, 0);
复制代码

为何 setTimeout 可以强迫 setState 同步更新 state 呢?

能够这么理解,当 React 调用某个组件的生命周期函数或者事件处理函数时,React 会想:“嗯,这一次函数可能调用屡次 setState,我会先打开一个标记,只要这个标记是打开的,全部的 setState 调用都是往任务队列里听任务,当这一次函数调用结束的时候,我再去批量处理任务队列,而后把这个标记关闭。”

由于 setTimeout 是一个 JS 函数,和 React 无关,对于 setTimeout 的第一个函数参数,这个函数参数的执行时机,已经不是 React 可以控制的了,换句话说,React 不知道何时这个函数参数会被执行,因此那个“标记”也没有打开。

当那个“标记”没有打开时,setState 就不会给任务列表里增长任务,而是强行马上更新 state 和引起从新渲染。这种状况下,React 认为:“这个 setState 发生在本身控制能力以外,也许开发者就是想要强行同步更新呢,宁滥勿缺,那就同步更新了吧。”

虽然有办法同步更新state,但要谨慎使用。

React 选择不一样步更新 state,是一种性能优化。

并且,每当你以为须要同步更新 state 的时候,每每说明你的代码设计存在问题,绝大部分状况下,你所须要的,并非“state 马上更新”,而是,“肯定 state 更新以后我要作什么”,这就引出了 setState 另外一个功能。

setState 的第二个参数

setState 的第二个参数能够是一个回调函数,当 state 真的被修改时,这个回调函数会被调用。

console.log(this.state.count); // 0
this.setState({count: 1}, () => {
  console.log(this.state.count); // 这里就是1了
})
console.log(this.state.count); // 依然为0
复制代码

当 setState 的第二个参数被调用时,React 已经处理完了任务列表,因此 this.state 就是更新后的数据。

若是须要在 state 更新以后作点什么,请利用第二个参数。

函数式 setState

setState 的第一个参数除了能够是对象,其实也能够传入一个函数。

当 setState 的第一个参数为函数时,任务列表上增长的就是一个可执行的任务函数了,React 每处理完一个任务,都会更新 this.state,而后把新的 state 传递给这个任务函数。

setState 第一个参数的形式以下:

function increment(state, props) {
  return {count: state.count + 1};
}
复制代码

能够看到,这是一个纯函数,不光接受当前的 state,还接受组件的 props,在这个函数中能够根据 state 和 props 任意计算,返回的结果会用于修改 this.state。

如此一来,咱们就能够这样连续调用 setState:

this.setState(increment);
this.setState(increment);
this.setState(increment);
复制代码

用这种函数式方式连续调用 setState,就真的可以让 this.state.count 增长 3,而不仅是增长 1。


最佳实践

最佳实践回答“怎么能用好”的问题,反映你实践经验的丰富程度。


组件设计模式

聪明组件和傻瓜组件

聪明组件和傻瓜组件:让咱们更好的组织代码


在 React 应用中,最简单也是最经常使用的一种组件模式,就是“聪明组件和傻瓜组件”。

软件设计中有一个原则,叫作“责任分离”,简单说就是让一个模块的责任尽可能少,若是发现一个模块功能过多,就应该拆分为多个模块,让一个模块都专一于一个功能,这样更利于代码的维护。

使用 React 来作界面,无外乎就是得到驱动界面的数据,而后利用这些数据来渲染界面。

把获取和管理数据的逻辑放在父组件,也就是聪明组件;把渲染界面的逻辑放在子组件,也就是傻瓜组件。

这么作的好处,是能够灵活地修改数据状态管理方式,好比,最初你可能用 Redux 来管理数据,而后你想要修改成用 Mobx,若是按照这种模式分割组件,那么,你须要改的只有聪明组件,傻瓜组件能够保持原状。

由于傻瓜组件通常没有本身的状态,咱们能够利用 PureComponent 来提升傻瓜组件的性能。

PureComponent 帮咱们处理了shouldComponentUpdate。

值得一提的是,PureComponent 中 shouldComponentUpdate 对 props 作得只是浅层比较,不是深层比较,若是 props 是一个深层对象,就容易产生问题。

好比,两次渲染传入的某个 props 都是同一个对象,可是对象中某个属性的值不一样,这在 PureComponent 眼里,props 没有变化,不会从新渲染,可是这明显不是咱们想要的结果。

虽然 PureComponent 能够提升组件渲染性能,可是它也不是没有代价的,它逼迫咱们必须把组件实现为 class,不能用纯函数来实现组件。

若是你使用 React v16.6.0 以后的版本,能够使用一个新功能 React.memo 来完美实现 React 组件,好比:

const Joke = React.memo(() => (
    <div> <img src={SmileFace} /> {this.props.value || 'loading...' } </div> )); 复制代码

高阶组件

高阶组件:让咱们更好的抽象公共逻辑


在开发 React 组件过程当中,很容易发现这样一种现象,某些功能是多个组件通用的,若是每一个组件都重复实现这样的逻辑,确定十分浪费,并且违反了“不要重复本身”(DRY,Don't Repeat Yourself)的编码原则,咱们确定想要把这部分共用逻辑提取出来重用。

咱们说过,在 React 的世界里,组件是第一公民,首先想到的是固然是把共用逻辑提取为一个 React 组件。不过,有些状况下,这些共用逻辑还无法成为一个独立组件,换句话说,这些共用逻辑单独没法使用,它们只是对其余组件的功能增强。

举个例子,对于不少网站应用,有些模块都须要在用户已经登陆的状况下才显示。好比,对于一个电商类网站,“退出登陆”按钮、“购物车”这些模块,就只有用户登陆以后才显示,对应这些模块的 React 组件若是连“只有在登陆时才显示”的功能都重复实现,那就浪费了。

这时候,咱们就能够利用“高阶组件(HoC)”这种模式来解决问题。

高阶组件的基本形式

高阶组件,本质是一个函数,它接受至少一个 React 组件为参数,而且可以返回一个全新的 React 组件做为结果,固然,这个新产生的 React 组件是对做为参数的组件的包装,因此,有机会赋予新组件一些加强的“神力”。

一个最简单的高阶组件是这样的形式:

const withDoNothing = (Component) => {
  const NewComponent = (props) => {
    return <Component {...props} />; }; return NewComponent; }; 复制代码

有了高阶组件,咱们就能够用它来抽取共同逻辑。

高阶组件的高级用法

高阶组件只须要返回一个 React 组件便可,没人规定高阶组件只能接受一个 React 组件做为参数,彻底能够传入多个 React 组件给高阶组件。

咱们能够用高阶组件封装登陆与登出的逻辑,以下:

const withLoginAndLogout = (ComponentForLogin, ComponentForLogout) => {
  const NewComponent = (props) => {
    if (getUserId()) {
      return <ComponentForLogin {...props} />;
    } else {
      return <ComponentForLogout{...props} />;
    }
  }
  return NewComponent;
};
复制代码
链式调用高阶组件

高阶组件最巧妙的一点,是能够链式调用。

假设,你有三个高阶组件分别是 withOne、withTwo 和 withThree,那么,若是要赋予一个组件 X 三个高阶组件的超能力,能够连续调用高阶组件,以下:

const SuperX = withThree(withTwo(withOne(X)));
复制代码

高阶组件自己就是一个纯函数,纯函数是能够组合使用的,因此,咱们其实能够把多个高阶组件组合为一个高阶组件,而后用这一个高阶组件去包装X,代码以下:

const hoc = compose(withThree, withTwo, withOne);
const SuperX = hoc(X);
复制代码

在上面代码中使用的 compose,是函数式编程中很基础的一种方法,做用就是把多个函数组合为一个函数。

React 组件能够当作积木同样组合使用,如今有了 compose,咱们就能够把高阶组件也当作积木同样组合,进一步重用代码。

假如一个应用中多个组件都须要一样的多个高阶组件包装,那就能够用 compose 组合这些高阶组件为一个高阶组件,这样在使用多个高阶组件的地方实际上就只须要使用一个高阶组件了。

不要滥用高阶组件

高阶组件虽然能够用一种可重用的方式扩充现有 React 组件的功能,但高阶组件并非绝对完美的。

首先,高阶组件不得不处理 displayName,否则 debug 会很痛苦。当 React 渲染出错的时候,靠组件的 displayName 静态属性来判断出错的组件类,而高阶组件老是创造一个新的 React 组件类,因此,每一个高阶组件都须要处理一下 displayName。

若是要作一个最简单的什么加强功能都没有的高阶组件,也必需要写下面这样的代码:

const withExample = (Component) => {
  const NewComponent = (props) => {
    return <Component {...props} />; } NewComponent.displayName = `withExample(${Component.displayName || Component.name || 'Component'})`; return NewCompoennt; }; 复制代码

对于 React 生命周期函数,高阶组件不用怎么特殊处理,可是,若是内层组件包含定制的静态函数,这些静态函数的调用在 React 生命周期以外,那么高阶组件就必需要在新产生的组件中增长这些静态函数的支持,这更加麻烦。关于这点,请参考这里

其次,高阶组件支持嵌套调用,这是它的优点。可是若是真的一大长串高阶组件被应用的话,当组件出错,你看到的会是一个超深的 stack trace,十分痛苦。

关于高阶组件的进阶内容,请参考高阶组件实现方法


render props 模式

render props 模式:让咱们更好的抽象公共逻辑,同时又避免了高阶组件的一些问题


高阶组件并非 React 中惟一的重用组件逻辑的方式,且高阶组件存在一些弊端,因此又诞生了render props 模式,也称为“以函数为子组件”的模式。

所谓 render props,指的是让 React 组件的 props 支持函数这种模式。由于做为 props 传入的函数每每被用来渲染一部分界面,因此这种模式被称为 render props。

一个最简单的 render props 组件 RenderAll,代码以下:

const RenderAll = (props) => {
  return(
     <React.Fragment> {props.children(props)} </React.Fragment> ); }; 复制代码

这个 RenderAll 预期子组件是一个函数,它所作的事情就是把子组件当作函数调用,调用参数就是传入的 props,而后把返回结果渲染出来,除此以外什么事情都没有作。

使用 RenderAll 的代码以下:

<RenderAll>
  {() => <h1>hello world</h1>}
</RenderAll>
复制代码

能够看到,RenderAll 的子组件,也就是夹在 RenderAll 标签之间的部分,实际上是一个函数。这个函数渲染出 <h1>hello world</h1>,这就是上面使用 RenderAll 渲染出来的结果。

固然,这个 RenderAll 没作任何实际工做,接下来咱们看 render props 真正强悍的使用方法。

传递 props

下面是实现 render props 的 Login 组件,能够看到,render props 和高阶组件的第一个区别,就是 render props 是真正的 React 组件,而不是一个返回 React 组件的函数。

const Login = (props) => {
  const userName = getUserName();

  if (userName) {
    const allProps = {userName, ...props};
    return (
      <React.Fragment> {props.children(allProps)} </React.Fragment> ); } else { return null; } }; 复制代码

当用户处于登陆状态,getUserName 返回当前用户名,不然返回空,而后咱们根据这个结果决定是否渲染 props.children 返回的结果。

固然,render props 彻底能够决定哪些 props 能够传递给 props.children,在 Login 中,咱们把 userName 做为增长的 props 传递给下去,这样就是 Login 的加强功能。

一个使用上面 Login 的 JSX 代码示例以下:

<Login>
  {({userName}) => <h1>Hello {userName}</h1>}
</Login>
复制代码
不局限于 children

render props 这个模式没必要局限于 children 这一个 props,任何一个 props 均可以做为函数,也能够利用多个 props 来做为函数。

咱们来扩展 Login,不光在用户登陆时显示一些东西,也能够定制用户没有登陆时显示的东西,咱们把这个组件叫作 Auth,对应代码以下:

const Auth= (props) => {
  const userName = getUserName();

  if (userName) {
    const allProps = {userName, ...props};
    return (
      <React.Fragment>
        {props.login(allProps)}
      </React.Fragment>
    );
  } else {
    <React.Fragment>
      {props.nologin(props)}
    </React.Fragment>
  }
};
复制代码

用法以下:

<Auth
  login={({userName}) => <h1>Hello {userName}</h1>}
  nologin={() => <h1>Please login</h1>}
/>
复制代码
依赖注入

render props 其实就是 React 世界中的“依赖注入”。

所谓依赖注入,指的是解决这样一个问题:逻辑 A 依赖于逻辑 B,若是让 A 直接依赖于 B,固然可行,可是 A 就无法作得通用了。依赖注入就是把 B 的逻辑以函数形式传递给 A,A 和 B 之间只须要对这个函数接口达成一致就行,如此一来,再来一个逻辑 C,也能够用同样的方法重用逻辑 A。

在上面的代码示例中,Login 和 Auth 组件就是上面所说的逻辑 A,而传递给组件的函数类型 props,就是逻辑 B 和 C。

render props 和高阶组件的比较

首先,render props 模式的应用,就是作一个 React 组件,而高阶组件,虽然名为“组件”,其实只是一个产生 React 组件的函数。

render props 不像高阶组件有那么多毛病,若是说 render props 有什么缺点,那就是 render props 不能像高阶组件那样链式调用,固然,这并非一个致命缺点。

render props 相对于高阶组件还有一个显著优点,就是 props 传递更加灵活。

总结:当须要重用 React 组件的逻辑时,建议首先看这个功能是否能够抽象为一个简单的组件;若是行不通的话,考虑是否能够应用 render props 模式;再不行的话,才考虑应用高阶组件模式


mixin

mixin:同样能够抽象公共逻辑,但如今已经不提倡使用。这里只作简单介绍。


React 在使用 createClass 构建组件时提供了 mixin 属性。mixin有两个做用:

  • 共享工具方法。
  • 生命周期继承,props 与 state 合并。

ES6 Classes 不支持 mixin。

mixin 的问题:

  • 破坏了原有组件的封装:mixin 方法会混入方法,给原有组件带来新的特性,但它也可能带来了新的 state 和 props,这意味着组件有一 些“不可见”的状态须要咱们去维护,但咱们在使用的时候并不清楚。另外,mixin 也有可能去依赖其余的 mixin,这样会创建一个 mixin 的依赖链,当咱们改动其 中一个 mixin 的状态时,极可能会直接影响其余的 mixin。
  • 命名冲突:尽管咱们能够经过更更名字来解决,但遇到第三方引用,或已经引用了几个 mixin 的状况下, 老是要花必定的成本去解决冲突。
  • 增长复杂性
高阶组件与mixin的比较

高阶组件与 mixin 的不一样之处



高阶组件符合函数式编程思想。对于原组件来讲,并不会感知到高阶组件的存在,只须要把功能套在它之上就能够了,从而避免了使用 mixin 时产生的反作用。


提供者模式

提供者模式:让咱们更好的跨层级传递数据


在 React 中,props 是组件之间通信的主要手段,可是,有一种场景单纯靠 props 来通信是不恰当的,那就是两个组件之间间隔着多层其余组件,下面是一个简单的组件树示例图图:



在上图中,组件 A 须要传递信息给组件 X,若是经过 props 的话,那么从顶部的组件 A 开始,要把 props 传递给组件 B,而后组件 B 传递给组件 D,最后组件 D 再传递给组件 X。

其实组件 B 和组件 D 彻底用不上这些 props,可是又被迫传递这些 props,这明显不合理,要知道组件树的结构会变化的,未来若是组件 B 和组件 D 之间再插入一层新的组件,这个组件也须要传递这个 props,这就麻烦无比。

可见,对于跨级的信息传递,咱们须要一个更好的方法。

在 React 中,解决这个问题应用的就是“提供者模式”。

提供者模式

提供者模式有两个角色,一个叫“提供者”(Provider),另外一个叫“消费者”(Consumer)。在上面的组件树中,组件 A 能够做为提供者,组件 X 就是消费者。

既然名为“提供者”,它能够提供一些信息,并且这些信息在它之下的全部组件,不管隔了多少层,均可以直接访问到,而不须要经过 props 层层传递。

避免 props 逐级传递,便是提供者的用途。

如何实现提供者模式

实现提供者模式,须要 React 的 Context 功能,能够说,提供者模式只不过是让 Context 功能更好用一些而已。

所谓 Context 功能,就是可以创造一个“上下文”,在这个上下文笼罩之下的全部组件均可以访问一样的数据。

提供者模式的一个典型用例就是实现“样式主题”(Theme),由顶层的提供者肯定一个主题,下面的样式就能够直接使用对应主题里的样式。这样,当须要切换样式时,只须要修改提供者就行,其余组件不用修改。

React v16.3.0 以前的提供者模式

在 React v16.3.0 以前,要实现提供者,就要实现一个 React 组件,不过这个组件要作两个特殊处理。

  • 须要实现 getChildContext 方法,用于返回“上下文”的数据;
  • 须要定义 childContextTypes 属性,声明“上下文”的结构。

下面就是一个实现“提供者”的例子,组件名为 ThemeProvider:

class ThemeProvider extends React.Component {
  getChildContext() {
    return {
      theme: this.props.value
    };
  }

  render() {
    return (
      <React.Fragment> {this.props.children} </React.Fragment> ); } } ThemeProvider.childContextTypes = { theme: PropTypes.object }; 复制代码

对于 ThemeProvider,咱们创造了一个上下文,这个上下文就是一个对象,结构是这样:

{
  theme: {
    //一个对象
  }
}
复制代码

接下来,就是使用这个“上下文”的组件。这里有两种方式:

使用类的方式:

class Subject extends React.Component {
  render() {
    const {mainColor} = this.context.theme;
    return (
      <h1 style={{color: mainColor}}> {this.props.children} </h1>
    );
  }
}

Subject.contextTypes = {
  theme: PropTypes.object
}
复制代码

使用纯函数组件:

const Paragraph = (props, context) => {
  const {textColor} = context.theme;
  return (
    <p style={{color: textColor}}> {props.children} </p>
  );
};

Paragraph.contextTypes = {
  theme: PropTypes.object
};
复制代码

这两种方式访问“上下文”的方式有些不一样,都必须增长 contextTypes 属性,必须和 ThemeProvider 的 childContextTypes 属性一致,否则,this.context 就不会获得任何值。

最后,咱们看如何结合”提供者“和”消费者“。

咱们作一个组件来使用 Subject 和 Paragraph,这个组件不须要帮助传递任何 props,代码以下:

const Page = () => (
  <div> <Subject>这是标题</Subject> <Paragraph> 这是正文 </Paragraph> </div>
);
复制代码

上面的组件 Page 使用了 Subject 和 Paragraph,如今咱们想要定制样式主题,只须要在 Page 或者任何须要应用这个主题的组件外面包上 ThemeProvider,对应的 JSX 代码以下:

<ThemeProvider value={{mainColor: 'green', textColor: 'red'}} >
  <Page /> </ThemeProvider>
复制代码

当咱们须要改变一个样式主题的时候,改变传给 ThemeProvider的 value 值就搞定了。

React v16.3.0 以后的提供者模式

首先,要用新提供的 createContext 函数创造一个“上下文”对象。

const ThemeContext = React.createContext();
复制代码

这个“上下文”对象 ThemeContext 有两个属性,分别就是——对,你没猜错——Provider 和 Consumer。

const ThemeProvider = ThemeContext.Provider;
const ThemeConsumer = ThemeContext.Consumer;
复制代码

使用“消费者”以下:

const Paragraph = (props, context) => {
  return (
    <ThemeConsumer> { (theme) => ( <p style={{color: theme.textColor}}> {props.children} </p> ) } </ThemeConsumer>
  );
};
复制代码

实现 Page 的方式并无变化:

<ThemeProvider value={{mainColor: 'green', textColor: 'red'}} >
  <Page /> </ThemeProvider>
复制代码
两种提供者模式实现方式的比较

在老版 Context API 中,“上下文”只是一个概念,并不对应一个代码,两个组件之间达成一个协议,就诞生了“上下文”。

在新版 Context API 中,须要一个“上下文”对象(上面的例子中就是 ThemeContext),使用“提供者”的代码和“消费者”的代码每每分布在不一样的代码文件中,那么,这个 ThemeContext 对象放在哪一个代码文件中呢?

最好是放在一个独立的文件中,这么一来,就多出一个代码文件,并且全部和这个“上下文”相关的代码,都要依赖于这个“上下文”代码文件,虽然这没什么大不了的,可是的确多了一层依赖关系。

为了不依赖关系复杂,每一个应用都不要滥用“上下文”,应该限制“上下文”的使用个数。


组合组件

组合组件:简化父组件向子组件传递props的方式


组合组件模式要解决的是这样一类问题:父组件想要传递一些信息给子组件,可是,若是用 props 传递又显得十分麻烦。

利用 Context 能够解决问题,但很是繁琐,组合组件让咱们能够用更简洁的方式去实现。

问题描述

不少界面都有 Tab 这样的元件,咱们须要一个 Tabs 组件和 TabItem 组件,Tabs 是容器,TabItem 是一个一个单独的 Tab,由于一个时刻只有一个 TabItem 被选中,很天然但愿被选中的 TabItem 样式会和其余 TabItem 不一样。

这并非一个很难的功能,首先咱们想到的就是,用 Tabs 中一个 state 记录当前被选中的 Tabitem 序号,而后根据这个 state 传递 props 给 TabItem,固然,还要传递一个 onClick 事件进去,捕获点击选择事件。

按照这样的设计,Tabs 中若是要显示 One、Two、Three 三个 TabItem,JSX 代码大体这么写:

<TabItem active={true} onClick={this.onClick}>One</TabItem>
<TabItem active={false} onClick={this.onClick}>Two</TabItem>
<TabItem active={false} onClick={this.onClick}>Three</TabItem> 
复制代码

这样写能够实现功能,但未免过于繁琐,且不利于维护。咱们但愿能够简单点,最好代码就这样:

<Tabs>
  <TabItem>One</TabItem>
  <TabItem>Two</TabItem>
  <TabItem>Three</TabItem>
</Tabs>
复制代码

相似这种场景,父子组件不经过 props 传递,两者之间有某种神秘的“组合”,就是咱们所说的“组合组件”。

实现方式

咱们先写出 TabItem 的代码,以下:

const TabItem = (props) => {
  const {active, onClick} = props;
  const tabStyle = {
    'max-width': '150px',
    color: active ? 'red' : 'green',
    border: active ? '1px red solid' : '0px',
  };
  return (
    <h1 style={tabStyle} onClick={onClick}> {props.children} </h1>
  );
};
复制代码

有了 TabItem ,咱们再看下 TabItem 的调用方式。

<Tabs>
  <TabItem>One</TabItem>
  <TabItem>Two</TabItem>
  <TabItem>Three</TabItem>
</Tabs>
复制代码

没有 props 传递,怎么悄无声息地把 active 和 onClick 传递给 TabItem ?

咱们能够把 props.children 拷贝一份,这样就有机会去篡改这份拷贝,最后渲染这份拷贝就行了。

咱们来看 Tabs 的实现代码:

class Tabs extends React.Component {
  state = {
    activeIndex:  0
  }

  render() {
    const newChildren = React.Children.map(this.props.children, (child, index) => {
      if (child.type) {
        return React.cloneElement(child, {
          active: this.state.activeIndex === index,
          onClick: () => this.setState({activeIndex: index})
        });
      } else {
        return child;
      }
    });

    return (
      <Fragment> {newChildren} </Fragment>
    );
  }
}
复制代码

在 render 函数中,咱们用了 React 中不经常使用的两个 API:

  • React.Children.map
  • React.cloneElement

使用 React.Children.map,能够遍历 children 中全部的元素,由于 children 多是一个数组嘛。

使用 React.cloneElement 能够复制某个元素。这个函数第一个参数就是被复制的元素,第二个参数能够增长新产生元素的 props,咱们就是利用这个机会,把 active 和 onClick 添加了进去。

这两个 API 双剑合璧,就能实现不经过表面的 props 传递,完成两个组件的“组合”。

实际应用

应用组合组件的每每是共享组件库,把一些经常使用的功能封装在组件里,让应用层直接用就行。在 antd 和 bootstrap 这样的共享库中,都使用了组合组件这种模式。


模式总结

所谓模式,就是特定于一种问题场景的解决办法。

模式(Pattern) = 问题场景(Context) + 解决办法(Solution)


若是不搞清楚场景,单纯知道有这么一个办法,就比如拿到了一杆枪殊不知道这杆枪用于打什么目标,是没有任何意义的。并非全部的枪都是同样的,有的枪擅长狙击,有的枪适合近战,有的枪只是发个信号。

模式就是咱们的武器,咱们必定要搞清楚一件武器应用的场合,才能真正发挥这件武器的威力。


高阶组件实现方法

实现高阶组件的方法有:

属性代理

定义:高阶组件经过被包裹的 React 组件来操做 props。

import React from 'react';

const MyContainer = WrapComponent =>
  class extends React.Component {
    render () {
      return <WrapComponent {...this.props} />; } }; 复制代码

高阶组件的做用有:控制 props、经过 refs 使用引用、抽象 state 和使用其余元素包裹。

控制 props

咱们能够读取、增长、编辑或是移除从 WrappedComponent 传进来的 props,但须要当心删除与编辑重要的 props。咱们应该尽量对高阶组件的 props 做新的命名以防止混淆。

经过 refs 使用引用

在高阶组件中,咱们能够接受 refs 使用WrappedComponent 的引用。

import React, {Component} from 'React';
const MyContainer = WrappedComponent =>
  class extends Component {
    proc (wrappedComponentInstance) {
      wrappedComponentInstance.method ();
    }
    render () {
      const props = Object.assign ({}, this.props, {
        ref: this.proc.bind (this),
      });
      return <WrappedComponent {...props} />; } }; 复制代码

当 WrappedComponent 被渲染时,refs 回调函数就会被执行,这样就会拿到一份Wrapped-Component 实例的引用。这就能够方便地用于读取或增长实例的 props,并调用实例的方法。

抽象 state

咱们能够经过 WrappedComponent 提供的 props 和回调函数抽象 state,高阶组件能够将原组件抽象为展现型组件,分离内部状态。

import React, {Component} from 'React';
const MyContainer = WrappedComponent =>
  class extends Component {
    constructor (props) {
      super (props);
      this.state = {
        name: '',
      };
      this.onNameChange = this.onNameChange.bind (this);
    }
    onNameChange (event) {
      this.setState ({
        name: event.target.value,
      });
    }
    render () {
      const newProps = {
        name: {
          value: this.state.name,
          onChange: this.onNameChange,
        },
      };
      return <WrappedComponent {...this.props} {...newProps} />; } }; 复制代码

咱们把 input 组件中对 name prop 的 onChange 方法提取到高阶组件中,这样就有效地抽象了一样的 state 操做。能够这么来使用它

import React, {Component} from 'React';

@MyContainer 
class MyComponent extends Component {
  render () {
    return <input name="name" {...this.props.name} />; } } 复制代码

经过这样的封装,咱们就获得了一个被控制的 input 组件。

使用其余元素包裹 WrappedComponent

咱们还能够使用其余元素来包裹 WrappedComponent,这既能够是为了加样式,也可 以是为了布局。

import React, {Component} from 'React';
const MyContainer = WrappedComponent =>
  class extends Component {
    render () {
      return (
        <div style={{display: 'block'}}> {' '}<WrappedComponent {...this.props} /> </div> ); } }; 复制代码

反向继承

定义:高阶组件继承于被包裹的 React 组件(从字面意思上看,它必定与继承特性相关)

简单例子:高阶组件返回的组件继承于 WrappedComponent。由于被动地继承了 WrappedComponent,全部的调用都会反向,这也是这种方法的由来。

const MyContainer = WrappedComponent =>
  class extends WrappedComponent {
    render () {
      return super.render ();
    }
  };
复制代码

在反向继承方法中,高阶组件能够使用 WrappedComponent 引用,这意味着它能够使用WrappedComponent 的 state、props 、生命周期和 render 方法。但它不能保证完整的子组件树被解析。

反向继承两大特色:

渲染劫持

渲染劫持指的就是高阶组件能够控制 WrappedComponent 的渲染过程,并渲染各类各样的结 果。咱们能够在这个过程当中在任何 React 元素输出的结果中读取、增长、修改、删除 props,或 读取或修改 React 元素树,或条件显示元素树,又或是用样式控制包裹元素树。

正如以前说到的,反向继承不能保证完整的子组件树被解析,这意味着将限制渲染劫持功能。 渲染劫持的经验法则是咱们能够操控 WrappedComponent 的元素树,并输出正确的结果。但若是 元素树中包括了函数类型的 React 组件,就不能操做组件的子组件。

const MyContainer = WrappedComponent =>
  class extends WrappedComponent {
    render () {
      if (this.props.loggedIn) {
        return super.render ();
      } else {
        return null;
      }
    }
  };
复制代码
控制 state

高阶组件能够读取、修改或删除WrappedComponent 实例中的 state,若是须要的话,也能够 增长 state。但这样作,可能会让WrappedComponent 组件内部状态变得一团糟。大部分的高阶组 件都应该限制读取或增长 state,尤为是后者,能够经过从新命名 state,以防止混淆。

const MyContainer = WrappedComponent =>
  class extends WrappedComponent {
    render () {
      return (
        <div> <h2>HOC Debugger Component</h2> <p>Props</p> {' '} <pre>{JSON.stringify (this.props, null, 2)}</pre> {' '} <p>State</p> <pre>{JSON.stringify (this.state, null, 2)}</pre> {' '} {super.render ()} </div>
      );
    }
  };
复制代码

状态管理·第三方工具

上文提到了状态管理·组件状态,接下来咱们来看看社区主流的状态管理工具。


Redux

理解 Redux

要理解 Redux,首先要明白咱们为何须要 Redux,或者说,Redux 适用于什么样的场景。

在真实应用中,React 组件树会很庞大很复杂,两个没有父子关系的 React 组件之间要共享信息,怎么办呢?

最直观的作法,就是将状态保存在一个全局对象中,这个对象,叫 store。

若是 store 是一个普通对象,谁均可以修改,那状态就乱套了。因此咱们要作一些限制,让 store 只接受特定事件,若是要修改 store 的数据,就往 store 发送这些事件, store 对事件进行响应,从而修改状态。

这里说的事件,就是 action ,而对应修改状态的函数,就是reducer。

Redux 的主要贡献,就是限制了对状态的修改方式,让全部改变均可以被追踪

适合 Redux 的场景

对于某个状态,究竟是放在 Redux 的 Store 中呢,仍是放在 React 组件自身的状态中呢?

针对这个问题,有如下原则:

第一步,看这个状态是否会被多个 React 组件共享。

第二步,看这个组件被 unmount 以后从新被 mount,以前的状态是否须要保留。

第三步,到这一步,基本上能够肯定,这个状态能够放在 React 组件中了。

Redux 和 React 结合的最佳实践

1、Store 上的数据应该范式化。

所谓范式化,就是尽可能减小冗余信息,像设计 MySQL 这样的关系型数据库同样设计数据结构。

2、使用 selector。

对于 React 组件,须要的是『反范式化』的数据,当从 Store 上读取数据获得的是范式化的数据时,须要经过计算来获得反范式化的数据。你可能会所以担忧出现问题,这种担忧不是没有道理,毕竟,若是每次渲染都要重复计算,这种浪费聚沙成塔可能真会产生性能影响,因此,咱们须要使用 seletor。业界应用最广的 selector 就是 reslector 。

reselector 的好处,是把反范式化分为两个步骤,第一个步骤是简单映射,第二个步骤是真正的重量级运算,若是第一个步骤发现产生的结果和上一次调用同样,那么第二个步骤也不用计算了,能够直接复用缓存的上次计算结果。

绝大部分实际场景中,老是只有少部分数据会频繁发生变化,因此 reselector 能够避免大量重复计算。

3、只 connect 关键点的 React 组件

当 Store 上状态发生改变的时候,全部 connect 上这个 Store 的 React 组件会被通知:『状态改变了!』

而后,这些组件会进行计算。connect 的实现方式包含 shouldComponentUpdate 的实现,能够阻挡住大部分没必要要的从新渲染,可是,毕竟处理通知也须要消耗 CPU,因此,尽可能让关键的 React 组件 connect 到 store 就行。

一个实际的例子就是,一个列表种可能包含几百个项,让每个项都去 connect 到 Store 上不是一个明智的设计,最好是只让列表去 connect,而后把数据经过 props 传递给各个项。

如何实现异步操做

使用 Redux 对于同步状态更新很是顺手,可是,遇到须要异步更新状态的场景,例如调用 AJAX 从服务器得到数据,这时候单用 Redux 就不够了,须要其余方式来辅助。

至今为止,还没法推荐一个杀手级的方法,各类方法都在吹嘘本身多厉害,可是任何一种方法都是易用性和复杂性的平衡。

最简单的 redux-thunk,代码量少,只有几行,用起来也很直观,可是开发者要写不少代码;而比较复杂的 redux-observable 至关强大,能够只用少许代码就实现复杂功能,可是前提是你要学会 RxJS,RxJS 自己学习曲线很陡,内容须要 一本书 的篇幅来介绍,这就是代价。

读者在本身的项目中,不管选择什么方式,必定要考虑这个方式的复杂度和学习成本。

在这里我不想过多介绍任何一种 Redux 扩展,由于任何一种都比不上 React 将要支持的 Suspense,Suspense 才是 React 中作异步操做的将来,在第 19 小节会详细介绍 Suspense。


Mobx

理解 Mobx

咱们用 Mobx 来实现一个很简单的计数工具,首先,须要有一个对象来记录计数值,代码以下:

import {observable} from 'mobx';

const counter = observable ({
  count: 0,
});
复制代码

在上面的代码中,counter 是一个对象,其实就是用 observable 函数包住一个普通 JS 对象,可是 observable 的介入,让 counter 对象拥有了神力。

咱们用最简单的代码来展现这种“神力”,代码以下:

import {autorun} from 'mobx';

window.counter = counter;

autorun (() => {
  console.log ('#count', counter.count);
});
复制代码

把 counter 赋值给 window.counter,是为了让咱们在 Chrome 的开发者界面能够访问。用 autorun 包住了一个函数,这个函数输出 counter.count 的值,这段代码的做用,咱们很快就能看到。

在 Chrome 的开发者界面,咱们能够直接访问 window.counter.count,神奇之处是,若是咱们直接修改 window.counter.count 的值,能够直接触发 autorun 的函数参数!


这个现象说明,mobx 的 observable 拥有某种“神力”,任何对这个对象的修改,都会马上引起某些函数被调用。和 observable 这个名字同样,被包装的对象变成了“被观察者”,而被调用的函数就是“观察者”,在上面的例子中,autorun 的函数参数就是“观察者”。

Mobx 这样的功能,等于实现了设计模式中的“观察者模式”(Observer Pattern),经过创建 observer 和 observable 之间的关联,达到数据联动。不过,传统的“观察者模式”要求咱们写代码创建二者的关联,也就是写相似下面的代码:

observable.register(observer);
复制代码

Mobx 最了不得之处,在于不须要开发者写上面的关联代码,Mobx本身经过解析代码就可以自动发现 observer 和 observable 之间的关系。

咱们很天然想到,若是让咱们的数据拥有这样的“神力”,那咱们就不用在修改完数据以后,再费心去调用某些函数使用这些数据了,数据管理会变得十分轻松。

用 decorator 来使用 Mobx

Mobx 和 React 并没有直接关系,为了创建两者的关系,须要安装 mobx-react

仍是以 Counter 为例,看如何用 decorator 使用 Mobx,咱们先看代码:

import {observable} from 'mobx';
import {observer} from 'mobx-react';

@observer 
class Counter extends React.Component {
  @observable count = 0;

  onIncrement = () => {
    this.count++;
  };

  onDecrement = () => {
    this.count--;
  };

  componentWillUpdate () {
    console.log ('#enter componentWillUpdate');
  }

  render () {
    return (
      <CounterView caption="With decorator" count={this.count} onIncrement={this.onIncrement} onDecrement={this.onDecrement} /> ); } } 复制代码

在上面的代码中,Counter 这个 React 组件自身是一个 observer,而 observable 是 Counter 的一个成员变量 count。

注意 observer 这 个decorator 来自于 mobx-react,它是 Mobx 世界和 React 的桥梁,被它“装饰”的组件,就是一个“观察者”。

本例中,成员变量 count 是被观察者,只要被观察者一变化,做为观察者的 Counter 组件就会从新渲染。

独立的 Store

真实的业务场景是一个状态须要多个组件共享,因此 observable 通常是在 React 组件以外。

咱们重写一遍 Counter 组件,代码以下:

const store = observable ({
  count: 0,
});
store.increment = function () {
  this.count++;
};
store.decrement = function () {
  this.count--;
}; // this decorator is must

@observer 
class Counter extends React.Component {
  onIncrement = () => {
    store.increment ();
  };

  onDecrement = () => {
    store.decrement ();
  };

  render () {
    return (
      <CounterView caption="With external state" count={store.count} onIncrement={this.onIncrement} onDecrement={this.onDecrement} /> ); } } 复制代码

Mobx 和 Redux 的比较

Mobx 和 Redux 的目标都是管理好应用状态,可是最根本的区别在于对数据的处理方式不一样。

Redux 认为,数据的一致性很重要,为了保持数据的一致性,要求Store 中的数据尽可能范式化,也就是减小一切没必要要的冗余,为了限制对数据的修改,要求 Store 中数据是不可改的(Immutable),只能经过 action 触发 reducer 来更新 Store。

Mobx 也认为数据的一致性很重要,可是它认为解决问题的根本方法不是让数据范式化,而是不要给机会让数据变得不一致。因此,Mobx 鼓励数据干脆就“反范式化”,有冗余没问题,只要全部数据之间保持联动,改了一处,对应依赖这处的数据自动更新,那就不会发生数据不一致的问题。

值得一提的是,虽然 Mobx 最初的一个卖点就是直接修改数据,可是实践中你们仍是发现这样无组织无纪律很差,因此后来 Mobx 仍是提供了 action 的概念。和 Redux 的 action 有点不一样,Mobx 中的 action 其实就是一个函数,不须要作 dispatch,调用就修改对应数据

若是想强制要求使用 action,禁止直接修改 observable 数据,使用 Mobx 的 configure,以下:

import {configure} from 'mobx';

configure({enforceActions: true});
复制代码

总结一下 Redux 和 Mobx 的区别,包括这些方面:

  • Redux 鼓励一个应用只用一个 Store,Mobx 鼓励使用多个 Store;
  • Redux 使用“拉”的方式使用数据,这一点和 React是一致的,但 Mobx 使用“推”的方式使用数据,和 RxJS 这样的工具走得更近;
  • Redux 鼓励数据范式化,减小冗余,Mobx 允许数据冗余,但一样能保持数据一致。

样式处理

基本样式设置

咱们能够使用 classnames 库来操做类。

// 若是不使用 classnames 库,就须要这样处理动态类名:
import React, {Component} from 'react';
class Button extends Component {
  render () {
    let btnClass = 'btn';
    if (this.state.isPressed) {
      btnClass += ' btn-pressed';
    } else if (this.state.isHovered) {
      btnClass += ' btn-over';
    }
    return <button className={btnClass}>{this.props.label}</button>;
  }
}
// 使用了 classnames 库代码后,就能够变得很简单: 
import React, { Component } from 'react';
import classNames from 'classnames'; 
class Button1 extends Component {
  // ...
  render () {
    const btnClass = classNames ({
      btn: true,
      'btn-pressed': this.state.isPressed,
      'btn-over': !this.state.isPressed && this.state.isHovered,
    });
    return <button className={btnClass}>{this.props.label}</button>;
  }
}
复制代码

CSS模块

CSS 模块化要解决两个问题:CSS 样式的导入与导出。灵活按需导入以便复用代码,导出时要可以隐藏内部做用域,以避免形成全局污染。

CSS 模块化解决方案有两种:Inline Style、CSS Modules。

Inline Style

这种方案完全抛弃 CSS,使用 JS 或 JSON 来写样式,能给 CSS 提供 JS 一样强大的模块化能力。但缺点一样明显,Inline Style 几乎不能利用 CSS 自己 的特性,好比级联、媒体查询(media query)等,:hover 和 :active 等伪类处理起来比较 复杂。另外,这种方案须要依赖框架实现,其中与 React 相关的有 Radium、jsxstyle 和 react-style。

CSS Modules

依旧使用 CSS,但使用 JS 来管理样式依赖。CSS Modules 能最大化地结合现有 CSS 生态和 JS 模块化能力,其 API 很是简洁,学习成本几乎为零。 发布时依旧编译出单独的 JS 和 CSS 文件。webpack css-loader 内置 CSS Modules 功能。


CSS Modules 注意事项

样式默认局部

使用了 CSS Modules 后,就至关于给每一个 class 名外加了 :local,以此来实现样式的局部化。若是咱们想切换到全局模式,能够使用 :global 包裹

使用 composes 来组合样式
/* components/Button.css */ 
.base { /* 全部通用的样式 */ }
.normal { 
  composes: base; 
  /* normal其余样式 */
}

.disabled {
  composes:base; 
  /* disabled 其余样式 */
}
复制代码

生成的 HTML 变为:

<button class="button--base-abc53 button--normal-abc53"> Processing... </button>
复制代码

因为在 .normal 中组合了 .base,因此编译后的 normal 会变成两个 class。 此外,使用 composes 还能够组合外部文件中的样式:

/* settings.css */
.primary-color {
  color: #f40; 
}
/* components/Button.css */ 
.base { /* 全部通用的样式 */ }
.primary {
  composes: base;
  composes: $primary-color from './settings.css'; /* primary 其余样式 */
}
复制代码
实现 CSS 与 JS 变量共享

:export 关键字能够把 CSS 中的变量输出到 JS 中

/* config.scss */ 
$primary-color: '#f40';
:export {
  primaryColor: $primary-color;
}
复制代码
/* app.js */
import style from 'config.scss';
// 会输出 #F40 console.log(style.primaryColor);
复制代码

React Router

随着 AJAX 技术的成熟,如今单页应用(Single Page Application)已是前端网页界的标配,名为“单页”,其实在设计概念上依然是多页的界面,只不过从技术层面上页之间的切换是没有总体网页刷新的,只须要作局部更新。

要实现“单页应用”,一个最要紧的问题就是作好“路由”(Routing),也就是处理好下面两件事:

  • 把 URL 映射到对应的页面来处理;
  • 页面之间切换作到只需局部更新。


react router v4 的动态路由

react-router 的 v3 和 v4 版完彻底全是不一样的两个工具,最大的区别是 v3 为静态路由, v4 作到了动态路由。

所谓“静态路由”,就是说路由规则是固定的。

所谓动态路由,指的是路由规则不是预先肯定的,而是在渲染过程当中肯定的。

使用

react-router 的工做方式,是在组件树顶层放一个 Router 组件,而后在组件树中散落着不少 Route 组件(注意比 Router 少一个“r”),顶层的 Router 组件负责分析监听 URL 的变化,在它保护伞之下的 Route 组件能够直接读取这些信息。

很明显,Router 和 Route 的配合,就是以前咱们介绍过的“提供者模式”,Router 是“提供者”,Route是“消费者”。

更进一步,Router 其实也是一层抽象,让下面的 Route 无需各类不一样 URL 设计的细节,不要觉得 URL 就一种设计方法,至少能够分为两种。

第一种很天然,好比 / 对应 Home 页,/about 对应 About 页,可是这样的设计须要服务器端渲染,由于用户可能直接访问任何一个 URL,服务器端必须能对 /的访问返回 HTML,也要对 /about 的访问返回 HTML。

第二种看起来不天然,可是实现更简单。只有一个路径 /,经过 URL 后面的 # 部分来决定路由,/#/ 对应 Home 页,/#/about 对应 About 页。由于 URL 中#以后的部分是不会发送给服务器的,因此,不管哪一个 URL,最后都是访问服务器的 / 路径,服务器也只须要返回一样一份 HTML 就能够,而后由浏览器端解析 # 后的部分,完成浏览器端渲染。

在 react-router,有 BrowserRouter 支持第一种 URL,有 HashRouter 支持第二种 URL。

动态路由

假设,咱们增长一个新的页面叫 Product,对应路径为 /product,可是只有用户登陆了以后才显示。若是用静态路由,咱们在渲染以前就肯定这条路由规则,这样即便用户没有登陆,也能够访问 product,咱们还不得不在 Product 组件中作用户是否登陆的检查。

若是用动态路由,则只须要在代码中的一处涉及这个逻辑:

<Switch>
  <Route exact path='/' component={Home}/>
  {
    isUserLogin() &&
    <Route exact path='/product' component={Product}/>,
  }  
  <Route path='/about' component={About}/>
</Switch>
复制代码

常见API

  1. <Link> :普通连接,不会触发浏览器刷新
  2. <NavLink>:相似<Link>可是会添加当前选中状态
  3. <Prompt>:知足条件时提示用户是否离开当前页面
  4. <Redirect>:重定向当前页面,例如登陆判断
  5. <Route>:路由配置的核心标记,路径匹配时显示对应组件
  6. <Switch>:只现实第一个匹配路由

React Router技巧

  1. 如何经过URL传递参数:
  2. 如何获取参数:this.props.match.params
  3. 路由匹配进阶资料

服务器端渲染

最近几年浏览器端框架很繁荣,以致于不少新入行的开发者只知道浏览器端渲染框架,都不知道存在服务器端渲染这回事,其实,网站应用最初全都是服务器端渲染,由服务器端用 PHP、Java 或者 Python 等其余语言产生 HTML 来给浏览器端解析。

相比于浏览器端渲染,服务器端渲染的好处是:

  1. 缩短首屏渲染时间
  2. 更好的搜索引擎优化

大致流程

React v16 以前的版本,代码是这样:

import React from 'react';
import ReactDOMServer from 'react-dom/server';

// 把产生html返回给浏览器端
const html = ReactDOMServer.renderToString(<Hello />); 复制代码

从 React v16 开始,上面的服务器端代码依然能够使用,可是也能够把 renderToString 替换为 renderToNodeStream,代码以下:

import React from 'react';
import ReactDOMServer from 'react-dom/server';

// 把渲染内容以流的形式塞给response
ReactDOMServer.renderToNodeStream(<Hello />).pipe(response); 复制代码

此外,浏览器端代码也有一点变化,ReactDOM.render 依然能够使用,可是官方建议替换为 ReactDOM.hydrate,原来的 ReactDOM.render 未来会被废弃掉。

renderToString 的功能是一口气同步产生最终 HTML,若是 React 组件树很庞大,这样一个同步过程可能比较耗时。

renderToNodeStream 把渲染结果以“流”的形式塞给 response 对象,这意味着不用等到全部 HTML 都渲染出来了才给浏览器端返回结果,“流”的做用就是有多少内容给多少内容,这样能够改进首屏渲染时间。

“脱水”和“注水”

React 有一个特色,就是把内容展现和动态功能集中在一个组件中。好比,一个 Counter 组件既负责怎么画出内容,也要负责怎么响应按键点击,这固然符合软件高内聚性的原则,可是也给服务器端渲染带来更多的工做。

设想一下,若是只使用服务器端渲染,那么产生的只有 HTML,虽然可以让浏览器端画出内容,可是,没有 JS 的辅助是没法响应用户交互事件的。

如何让页面响应用户事件?其实咱们已经作过这件事了,Counter 组件里面已经有对按钮事件的处理,咱们所要作的只是让 Counter 组件在浏览器端从新执行一遍,也就是 mount 一遍就能够了。

也就是说,若是想要动态交互效果,使用 React 服务器端渲染,必须也配合使用浏览器端渲染

如今问题变得更加有趣了,在服务器端咱们给 Counter 一个初始值(这个值能够不是缺省的 0),让 Counter 渲染产生 HTML,这些 HTML 要传递给浏览器端,为了让 Counter 的 HTML“活”起来点击相应事件,必需要在浏览器端从新渲染一遍 Counter 组件。在浏览器端渲染 Counter 以前,用户就能够看见 Counter 组件的内容,可是没法点击交互,要想点击交互,就必需要等到浏览器端也渲染一次 Counter 以后。

接下来的一个问题,若是服务器端塞给 Counter 的数据和浏览器端塞给 Counter 的数据不同呢?

在 React v16 以前,React 在浏览器端渲染以后,会把内容和服务器端给的 HTML 作一个比对。若是彻底同样,那最好,接着用服务器端 HTML 就行了;若是有一丁点不同,就会马上丢掉服务器端的 HTML,从新渲染浏览器端产生的内容,结果就是用户能够看到界面闪烁。由于 React 抛弃的是整个服务器端渲染内容,组件树越大,这个闪烁效果越明显。

React 在 v16 以后,作了一些改进,再也不要求整个组件树两端渲染结果分绝不差,可是若是发生不一致,依然会抛弃局部服务器端渲染结果。

总之,若是用服务器端渲染,必定要让服务器端塞给 React 组件的数据和浏览器端一致

为了达到这一目的,必须把传给 React 组件的数据给保留住,随着 HTML 一块儿传递给浏览器网页,这个过程,叫作“脱水”(Dehydrate);在浏览器端,就直接拿这个“脱水”数据来初始化 React 组件,这个过程叫“注水”(Hydrate)。

前面提到过 React v16 以后用 React.hydrate 替换 React.render,这个 hydrate 就是“注水”。


总之,为了实现React的服务器端渲染,必需要处理好这两个问题:

  • 脱水
  • 注水

同构应用

有不少文章都提到同构应用,其实就是首屏使用服务端渲染,后面使用浏览器端渲染的应用。



使用 Next.js 实现服务端渲染

上文提到服务器端渲染,不过,服务器端渲染的问题并不这么简单,一个最直接的问题,就是怎么处理多个页面的『单页应用』?

所谓单页应用,就是虽然用户感受有多个页面,可是实现上只有一个页面,用户感受到页面能够来回切换,但其实只是一个页面并无彻底刷新,只是局部界面更新而已。

假设一个单页应用有三个页面 Home、Prodcut 和 About,分别对应的的路径是 /home、/product 和 /about,并且三个页面都依赖于 API 调用来获取外部数据。

如今咱们要作服务器端渲染,若是只考虑用户直接在地址栏输入 /home、/product 和 /about 的场景,很容易知足,按照上面说的套路作就是了。可是,这是一个单页应用,用户能够在 Home 页面点击连接无缝切换到 Product,这时候 Product 要作彻底的浏览器端渲染。换句话说,每一个页面都须要既支持服务器端渲染,又支持彻底的浏览器端渲染,更重要的是,对于开发者来讲,确定不但愿为了这个页面实现两套程序,因此必须有同时知足服务器端渲染和浏览器端渲染的代码表示方式。

getInitialProps

咱们经过一个简单的例子来说解Next.js中最重要的概念getInitialProps。

import React from 'react';

const timeout = (ms, result) => {
  return new Promise (resolve => setTimeout (() => resolve (result), ms));
};

const Home = props => (
  <h1> Hello {props.userName} </h1>
);

Home.getInitialProps = async () => {
  return await timeout (200, {userName: 'Morgan'});
};

export default Home;
复制代码

这里模拟了一个延时操做用以获取userName,并将其展现在页面上。

这段代码的关键在于getInitialProps。

这个 getiInitialProps 是 Next.js 最伟大的发明,它肯定了一个规范,一个页面组件只要把访问 API 外部资源的代码放在 getInitialProps 中就足够,其他的不用管,Next.js 天然会在服务器端或者浏览器端调用 getInitialProps 来获取外部资源,并把外部资源以 props 的方式传递给页面组件。

注意 getInitialProps 是页面组件的静态成员函数,也能够在组件类中加上 static 关键字定义。

class Home extends React.Component {
  static async getInitialProps () {}
}
复制代码

咱们能够这样来看待 getInitialProps,它就是 Next.js 对表明页面的 React 组件生命周期的扩充。React 组件的生命周期函数缺少对异步操做的支持,因此 Next.js 干脆定义出一个新的生命周期函数 getInitialProps,在调用 React 原生的全部生命周期函数以前,Next.js 会调用 getInitialProps 来获取数据,而后把得到数据做为 props 来启动 React 组件的原生生命周期过程

这个生命周期函数的扩充十分巧妙,由于:

  • 没有侵入 React 原生生命周期函数,之前的 React 组件该怎么写仍是怎么写;
  • getInitialProps 只负责获取数据的过程,开发者不用操心何时调用 getInitialProps,依然是 React 哲学的声明式编程方式;
  • getInitialProps 是 async 函数,能够利用 JS 语言的新特性,用同步的方式实现异步功能。

Next.js 的“脱水”和“注水”

咱们打开Next应用的网页源代码,能够看到相似下面的内容:

<script> __NEXT_DATA__ = { "props":{ "pageProps": {"userName":"Morgan"}}, "page":"/","pathname":"/","query":{},"buildId":"-","assetPrefix":"","nextExport":false,"err":null,"chunks":[]} </script>
复制代码

Next.js 在作服务器端渲染的时候,页面对应的 React 组件的 getInitialProps 函数被调用,异步结果就是“脱水”数据的重要部分,除了传给页面 React 组件完成渲染,还放在内嵌 script 的 NEXT_DATA 中,这样,在浏览器端渲染的时候,是不会去调用 getInitialProps 的,直接经过 NEXT_DATA 中的“脱水”数据来启动页面 React 组件的渲染。

这样一来,若是 getInitialProps 中有调用 API 的异步操做,只在服务器端作一次,浏览器端就不用作了。

那么,getInitialProps 何时会在浏览器端调用呢?

当在单页应用中作页面切换的时候,好比从 Home 页切换到 Product 页,这时候彻底和服务器端不要紧,只能靠浏览器端本身了,Product页面的 getInitialProps 函数就会在浏览器端被调用,获得的数据用来开启页面的 React 原生生命周期过程。

关键点是,浏览器可能会直接访问 /home 或者 /product,也可能经过网页切换访问这两个页面,也就是说 Home 或者 Product 均可能被服务器端渲染,也可能彻底只有浏览器端渲染,不过,这对应用开发者来讲无所谓,应用开发者只要写好 getInitialProps,至于调用 getInitialProps 的时机,交给 Next.js 处理就行了。


服务端渲染小结

react 提供了renderToString、renderToNodeStream、hydrate等API支持服务端渲染,但Facebook官方并无使用react的服务端渲染,致使react服务端渲染没有一个官方标准。

服务端渲染的思路并不难,就是在服务端渲染出HTML传给浏览器去解析。

难就难在,服务端渲染的数据从何而来。若是服务端渲染的数据和浏览器端渲染的数据不一致,浏览器端会从新执行渲染,页面会出现闪烁。

为何有了服务端渲染,还须要关注浏览器端渲染呢?

这是由于服务端渲染只是返回了HTML,页面能绘制出来,却没办法响应用户操做,因此必须从新进行浏览器端渲染,让页面能够正常响应用户操做。当浏览器端渲染出的HTML跟服务端返回的HTML不一致时,就会出现上面说的闪烁。

要解决这个问题,就引入了“注水”跟“脱水”的概念。

服务端渲染时,获取的数据,一方面用于生成最终的HTML,另外一方面,也会包含在HTML中返回给浏览器端,这个过程称为“脱水”。

浏览器端拿到脱水的数据进行渲染,就保证了渲染出的HTML跟服务端返回的一致,这个过程就是“注水”,涉及的API就是hydrate。

原理搞明白了,如何实施呢?若是咱们作的是单页应用,那问题更加麻烦。由于用户能够经过不一样的URL返回页面,这意味着咱们每一个页面都要既支持服务端渲染,也支持浏览器端渲染,但咱们确定不但愿为每一个页面写两份代码!

这就轮到 Next.js 登场了。

Next.js 是目前解决服务端渲染最好的框架。他经过增长 getInitialProps 这个API优雅的解决了上述的问题。

Next.js 将服务端渲染脱水后的数据,经过 NEXT_DATA 返回给浏览器端,首屏加载能够使用该数据进行渲染,就保证先后数据一致,页面不会闪烁。

浏览器可能会直接访问 /home 或者 /product,也可能经过网页切换访问这两个页面,也就是说 Home 或者 Product 均可能被服务器端渲染,也可能彻底只有浏览器端渲染,不过,这对应用开发者来讲无所谓,应用开发者只要写好 getInitialProps,至于调用 getInitialProps 的时机,交给 Next.js 处理就行了。

经常使用开发调试工具

开发react应用最经常使用的调试工具备:ESLint,Prettier,React DevTool,Redux DevTool

ESLint

  • 使用.eslintrc进行规则的配置
  • 使用airbnb的JS代码风格

Prettier

  • 代码格式化的神器
  • 保证更容易写出风格一致的代码

小技巧:在Chrome中监测react性能:在URL后加?react_perf,好比:localhost:3000/?react_perf


组件测试

React 让前端单元测试变得容易:

  • React 应用不多须要访问浏览器API
  • 虚拟DOM能够在nodejs环境运行和测试
  • Redux隔离了状态管理,纯数据层单元测试

React 组件的单元测试,只有三个要点:

  • 用 Jest;
  • 用 Enzyme;
  • 保持 100% 的代码覆盖率。

Jest

“测试驱动”难以开展,主要有几个缘由:

  • 单元测试用例庞大,执行时间过长。
  • 单元测试用例之间相互影响。

Jest 较好地解决了上面说的问题,由于 Jest 最重要的一个特性,就是支持并行执行。

Jest 为每个单元测试文件创造一个独立的运行环境,换句话说,Jest 会启动一个进程执行一个单元测试文件,运行结束以后,就把这个执行进程废弃了,这个单元测试文件即便写得比较差,把全局变量污染得一团糟,也不会影响其余单元测试文件,由于其余单元测试文件是用另外一个进程来执行。

更妙的是,由于每一个单元测试文件之间再无纠葛,Jest 能够启动多个进程同时运行不一样的文件,这样就充分利用了电脑的多 CPU 多核,单进程 100 秒才完成的测试执行过程,8 核只须要 12.5 秒,速度快了不少。

使用 create-react-app 产生的项目自带 Jest 做为测试框架,不奇怪,由于 Jest 和 React 同样都是出自 Facebook。

jest中文教程

Enzyme

Enzyme 是最受欢迎的 React 测试工具库。

Enzyme 的官网地址

代码覆盖率

一个应用不光全部的单元测试都要经过,并且全部单元测试都必须覆盖到代码 100% 的角落。

若是对覆盖率的要求低于 100%,时间一长,质量一定会愈来愈下滑

在 create-react-app 创造的应用中,已经自带了代码覆盖率的支持,运行下面的命令,不光会运行全部单元测试,也会获得覆盖率汇报。

npm test -- --coverage 代码覆盖率包含四个方面:

  • 语句覆盖率
  • 逻辑分支覆盖率
  • 函数覆盖率
  • 代码行覆盖率

只有四个方面都是 100%,才算真的 100%。


市场应用趋势

随着技术生态的发展,和应用问题的变迁,技术的应用场景和流行趋势会受到影响。这层回答“谁用,用在哪”的问题,反映你对技术应用领域的认识宽度。

相关文章
相关标签/搜索