React上手 —— 进阶篇

代码分割

使用Webpack或者Browserify这样的打包工具,最终会生成一个bundle.js,会一次性把代码都加载进来,但是随着项目的不断扩大, 一次性加载所有文件导致加载时间过长。为了避免搞出大体积的代码包,在前期就思考该问题并对代码包进行分割是个不错的选择。代码分割是由诸如 Webpack(代码分割)和 Browserify(factor-bundle)这类打包器支持的一项技术,能够创建多个包并在运行时动态加载。

import

import("./math").then(math => {
  console.log(math.add(16, 26));
});

如果你自己配置 Webpack,你可能要阅读下 Webpack 关于代码分割的指南。你的 Webpack 配置应该类似于此。

当使用 Babel 时,你要确保 Babel 能够解析动态 import 语法而不是将其进行转换。对于这一要求你需要 babel-plugin-syntax-dynamic-import 插件。

React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    
); }

Suspense

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    
Loading...
}>
); }

如果还没有加载完可以这么操作。

异常捕获边界(Error boundaries)

import MyErrorBoundary from './MyErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

const MyComponent = () => (
  
Loading...
}>
);

React.lazy 目前只支持默认导出(default exports)。如果需要使用命名导出需要增加中间模块:

// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));

Context

从基础篇我们可以看见,数据都是自定向上,但是由于项目的不断扩大,组件的层级也不断加深,有些数据是应该被共享的而不应该,一层层传递(维护成本太高),比如:主题颜色、用户信息、定位地区等。
如何使用:

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      
        
      
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    
); } class ThemedButton extends React.Component { // 指定 contextType 读取当前的 theme context。 // React 会往上找到最近的 theme Provider,然后使用它的值。 // 在这个例子中,当前的 theme 值为 “dark”。 static contextType = ThemeContext; render() { return

还有一种情况是,componentA 渲染 componentB ,componentB 渲染 componentC, componentC 渲染 componentD,而控制组件数据的是A,最终渲染的是D,这样的情况不需要Context而用组合组件是更优雅的方式:

function Page(props) {
  const user = props.user;
  const userLink = (
    
      
    
  );
  return ;
}

// 现在,我们有这样的组件:

// ... 渲染出 ...

// ... 渲染出 ...

// ... 渲染出 ...
{props.userLink}

即在A里就定义好组件D,将组件D一层一层传递下去。
但是如果是很多组件,不同层级需要相同数据还是使用Context比较好。

错误边界

注意
错误边界无法捕获以下场景中产生的错误:

  • 事件处理(了解更多)
  • 异步代码(例如 setTimeoutrequestAnimationFrame 回调函数)
  • 服务端渲染
  • 它自身抛出来的错误(并非它的子组件)

请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return 

Something went wrong.

; } return this.props.children; } }

然后你可以将它作为一个常规组件去使用:


  

Refs 转发

当我们需要控制一个封装的组件的焦点时,比如input或者button,我们需要那到这个组件的实例就是ref。来进行操作,react提供了一个方法来来转发ref。

const FancyButton = React.forwardRef((props, ref) => (
  
));

// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
Click me!;
  • 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
  • 我们通过指定 ref 为 JSX 属性,将其向下传递给
  • React 传递 ref 给 fowardRef 内函数 (props, ref) => ...,作为其第二个参数。
  • 我们向下转发该 ref 参数到
  • 当 ref 挂载完成,ref.current 将指向
function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      return ;
    }
  }

  return LogProps;
}

注意:refs 将不会透传下去。这是因为 ref 不是 prop 属性。就像 key 一样,其被 React 进行了特殊处理。如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。

Fragments

React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。

render() {
  return (
    
      
      
      
    
  );
}

最终不会渲染Fragment,只有children。

高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

HOC 是纯函数,没有副作用。

比如A组件需要发布订阅,组件B需要发布订阅,甚至更多的组件需要一个相似的功能,如果每次我们都是在每个组件里写的话维护成本太高,效率太低,我们希望我们只写base组件,而用高级组件给包裹一下就能都拥有这个逻辑:

我们可以编写一个创建组件的函数(高级函数),比如 CommentList 和 BlogPost,订阅 DataSource。该函数将接受一个子组件作为它的其中一个参数,该子组件将订阅数据作为 prop。让我们调用函数 withSubscription:

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

当渲染 CommentListWithSubscription 和 BlogPostWithSubscription 时, CommentList 和 BlogPost 将传递一个 data prop,其中包含从 DataSource 检索到的最新数据:

// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
  // ...并返回另一个组件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ...负责订阅相关的操作...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... 并使用新数据渲染被包装的组件!
      // 请注意,我们可能还会传递其他属性
      return ;
    }
  };
}

Portals

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。
这包含事件冒泡。一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先。假设存在如下 HTML 结构:


  
    

在 #app-root 里的 Parent 组件能够捕获到未被捕获的从兄弟节点 #modal-root 冒泡上来的事件。

// 在 DOM 中有两个容器是兄弟级 (siblings)
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement('div');
  }

  componentDidMount() {
    // 在 Modal 的所有子元素被挂载后,
    // 这个 portal 元素会被嵌入到 DOM 树中,
    // 这意味着子元素将被挂载到一个分离的 DOM 节点中。
    // 如果要求子组件在挂载时可以立刻接入 DOM 树,
    // 例如衡量一个 DOM 节点,
    // 或者在后代节点中使用 ‘autoFocus’,
    // 则需添加 state 到 Modal 中,
    // 仅当 Modal 被插入 DOM 树中才能渲染子元素。
    modalRoot.appendChild(this.el);
  }

  componentWillUnmount() {
    modalRoot.removeChild(this.el);
  }

  render() {
    return ReactDOM.createPortal(
      this.props.children,
      this.el,
    );
  }
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {clicks: 0};
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 当子元素里的按钮被点击时,
    // 这个将会被触发更新父元素的 state,
    // 即使这个按钮在 DOM 中不是直接关联的后代
    this.setState(state => ({
      clicks: state.clicks + 1
    }));
  }

  render() {
    return (
      

Number of clicks: {this.state.clicks}

Open up the browser DevTools to observe that the button is not a child of the div with the onClick handler.

); } } function Child() { // 这个按钮的点击事件会冒泡到父元素 // 因为这里没有定义 'onClick' 属性 return (
); } ReactDOM.render(, appRoot);

Refs and the DOM

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
何时使用 Refs:

  • 下面是几个适合使用 refs 的情况:
  • 管理焦点,文本选择或媒体播放。
  • 触发强制动画。
  • 集成第三方 DOM 库。

创建refs:

16.3以后:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return 
; } }

16.3以前:回调创建

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);

    this.textInput = null;

    this.setTextInputRef = element => {
      this.textInput = element;
    };

    this.focusTextInput = () => {
      // 使用原生 DOM API 使 text 输入框获得焦点
      if (this.textInput) this.textInput.focus();
    };
  }

  componentDidMount() {
    // 组件挂载后,让文本框自动获得焦点
    this.focusTextInput();
  }

  render() {
    // 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React
    // 实例上(比如 this.textInput)
    return (
      
); } }

在 componentDidMount 或 componentDidUpdate 触发前,React 会保证 refs 一定是最新的。
你可以在组件间传递回调形式的 refs,就像你可以传递通过 React.createRef() 创建的对象 refs 一样。

function CustomTextInput(props) {
  return (
    
); } class Parent extends React.Component { render() { return ( this.inputElement = el} /> ); } }

访问Refs

  • 原生元素:接受底层DOM作为current属性
  • class组件:接受组件实例作为current属性
  • 函数组件:不能在函数组件上创建refs

Render Props

术语 “render prop” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
解决什么问题需要这个Render Props呢?
比如,我有一个鼠标组件,他会记录每次用户的鼠标位置:

//  组件封装了我们需要的行为...
class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      
{/* ...但我们如何渲染

以外的东西? */}

The current mouse position is ({this.state.x}, {this.state.y})

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

移动鼠标!

); } }

现在我们需要实现,鼠标移动的时候有一只猫跟着鼠标,又或者其他组件会跟随鼠标,那么他们都需要鼠标的x,y。 如果仅仅只是一只猫跟着鼠标,那么还好,我们把鼠标和猫的代码写在一起就行了:

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      
    );
  }
}

class MouseWithCat extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      
{/* 我们可以在这里换掉

...... 但是接着我们需要创建一个单独的 每次我们需要使用它时, 是不是真的可以重复使用. */}

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

移动鼠标!

); } }

但是问题就在于有很多组件都需要x,y这俩值,如果我们都这么写毫无复用性可言,这个时候就出现了这个技术,render props:

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      
{/* Instead of providing a static representation of what renders, use the `render` prop to dynamically determine what to render. */} {this.props.render(this.state)}
); } } class MouseTracker extends React.Component { render() { return (

移动鼠标!

( )}/>
); } }

这样就实现了x,y的共享。
除此之外,我们并不一定要用render来命名,我们也可以用其他属性名,甚至是children:

render() {
    return (
      
{/* Instead of providing a static representation of what renders, use the `render` prop to dynamically determine what to render. */} {this.props.children(this.state)}
); }

移动鼠标!

{mouse => ( )}

Typescript

在 Create React App 中使用 TypeScript

npx create-react-app my-app --typescript

如需将 TypeScript 添加到现有的 Create React App 项目中,请参考此文档.

添加 TypeScript 到现有项目中

  • 安装typescript
npm install --save-dev typescript

恭喜!你已将最新版本的 TypeScript 安装到项目中。安装 TypeScript 后我们就可以使用 tsc 命令。在配置编译器之前,让我们将 tsc 添加到 package.json 中的 “scripts” 部分:

"scripts": {
    "build": "tsc",
    // ...
  },
  • 配置 TypeScript 编译器
npx tsc --init

tsconfig.json 文件中,有许多配置项用于配置编译器。查看所有配置项的的详细说明,请参考此文档。

  • 首先,让我们重新整理下项目目录,把所有的源代码放入 src 目录中。
  • 其次,我们将通过配置项告诉编译器源码和输出的位置。
// tsconfig.json

{
  "compilerOptions": {
    // ...
    "rootDir": "src",
    "outDir": "build"
    // ...
  },
}

类型定义

为了能够显示来自其他包的错误和提示,编译器依赖于声明文件。声明文件提供有关库的所有类型信息。这样,我们的项目就可以用上像 npm 这样的平台提供的三方 JavaScript 库。

Bundled
DefinitelyTyped :DefinitelyTyped 是一个庞大的声明仓库,为没有声明文件的 JavaScript 库提供类型定义。这些类型定义通过众包的方式完成,并由微信和开源贡献者一起管理。例如,React 库并没有自己的声明文件。但我们可以从 DefinitelyTyped 获取它的声明文件。只要执行以下命令。

# yarn
yarn add --dev @types/react

# npm
npm i --save-dev @types/react

你现在已做好编码准备了!我们建议你查看以下资源来了解有关 TypeScript 的更多知识:

  • TypeScript 文档:基本类型
  • TypeScript 文档:JavaScript 迁移
  • TypeScript 文档:React 与 Webpack

严格模式

import React from 'react';

function ExampleApplication() {
  return (
    
); }

在上述的示例中,不会对 Header 和 Footer 组件运行严格模式检查。但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查。
StrictMode 目前有助于:

  • 识别不安全的生命周期
  • 关于使用过时字符串 ref API 的警告
  • 关于使用废弃的 findDOMNode 方法的警告
  • 检测意外的副作用
  • 检测过时的 context API

使用 PropTypes 进行类型检查

PropTypes 提供一系列验证器,可用于确保组件接收到的数据类型是有效的。在本例中, 我们使用了 PropTypes.string。当传入的 prop 值类型不正确时,JavaScript 控制台将会显示警告。出于性能方面的考虑,propTypes 仅在开发模式下进行检查。

参考文献

https://react.docschina.org/

你可能感兴趣的:(React上手 —— 进阶篇)