Давайте поговорим о моем понимании и применении React Context

внешний интерфейс JavaScript React.js
Давайте поговорим о моем понимании и применении React Context

предисловие

ContextВ контексте контекста эта концепция часто встречается в мире программирования, а также присутствует в React.

В официальной документации React,ContextОн классифицируется как Advanced и относится к расширенному API React, но официально не рекомендуется использовать Context в стабильной версии приложения.

The vast majority of applications do not need to use content.

If you want your application to be stable, don't use context. It is an experimental API and it is likely to break in future releases of React.

Однако это не означает, что нам не нужно обращать вниманиеContext. Фактически, многие превосходные компоненты React дополняют свои собственные функции через контекст, например React-Redux.<Provider />, то есть черезContextобеспечить глобальныйstore, перетащите компонент react-dnd черезContextРаспределите события перетаскивания DOM в компоненте и направьте компонентный реактивный маршрутизатор черезContextУправление состоянием маршрутизации и многое другое. В разработке компонента React, если вы используете его правильноContext, может сделать ваши компоненты мощными и гибкими.

Я просто хочу поговорить с вами сегодня, что я узнал во время разработкиContextи как я использую его для разработки компонентов.

Примечание. Все приложения, упомянутые в этой статье, относятся к веб-приложениям.

Знакомство с контекстом React

Официальное определение контекста

Официальный сайт документации React неверенContextДает определение «что есть» и является скорее описанием использованияContextсценарии и как использоватьContext.

Официальный сайт для использованияContextСценарий описывается так:

In Some Cases, you want to pass data through the component tree without having to pass the props down manuallys at every level. you can do this directly in React with the powerful "context" API.

Проще говоря, когда вы не хотите проходить слой за слоем в дереве компонентовpropsилиstateспособ пройти данные, вы можете использоватьContextреализоватьпо уровнямпередача данных компонента.

Используйте реквизиты или состояние для передачи данных, и данные передаются сверху вниз.

использоватьContext, который может передавать данные между компонентами.

Как использовать контекст

Если вы хотитеContextДля работы необходимы два компонента, одинContextПроизводитель (Provider), обычно родительский узел, и еще одинContextПотребитель (Consumer), обычно один или несколько дочерних узлов. такContextиспользовать на основеМодель «производитель-потребитель».

Для родительского компонента этоContextПроизводитель, необходимо передать статическое свойствоchildContextTypesОбъявить предоставленные дочерним компонентамContextсвойства объекта и реализует экземплярgetChildContextметод, который возвращает делегатContextПростой объект (простой объект).

import React from 'react'
import PropTypes from 'prop-types'

class MiddleComponent extends React.Component {
  render () {
    return <ChildComponent />
  }
}

class ParentComponent extends React.Component {
  // 声明Context对象属性
  static childContextTypes = {
    propA: PropTypes.string,
    methodA: PropTypes.func
  }
  
  // 返回Context对象,方法名是约定好的
  getChildContext () {
    return {
      propA: 'propA',
      methodA: () => 'methodA'
    }
  }
  
  render () {
    return <MiddleComponent />
  }
}

И дляContextПотребитель получает доступ к родительскому компоненту следующим образом.Context.

import React from 'react'
import PropTypes from 'prop-types'

class ChildComponent extends React.Component {
  // 声明需要使用的Context属性
  static contextTypes = {
    propA: PropTypes.string
  }
  
  render () {
    const {
      propA,
      methodA
    } = this.context
    
    console.log(`context.propA = ${propA}`)  // context.propA = propA
    console.log(`context.methodA = ${methodA}`)  // context.methodA = undefined
    
    return ...
  }
}

Дочерние компоненты должны передавать статическое свойствоcontextTypesПосле объявления можно получить доступ к родительскому компонентуContextАтрибуты объекта, в противном случае, даже если имя атрибута правильное, полученный объектundefined.

Для сокращения без исключения (компонент без гражданства) можно получить доступ к родительскому компонентуContext

import React from 'react'
import PropTypes from 'prop-types'

const ChildComponent = (props, context) => {
  const {
    propA
  } = context
    
  console.log(`context.propA = ${propA}`)  // context.propA = propA
    
  return ...
}
  
ChildComponent.contextProps = {
  propA: PropTypes.string    
}

В следующем выпуске React будетContextAPI был скорректирован, чтобы сделать более понятным, как используется режим производитель-потребитель.

import React from 'react';
import ReactDOM from 'react-dom';

const ThemeContext = React.createContext({
  background: 'red',
  color: 'white'
});

через статический методReact.createContext()СоздаватьContextобъект, этоContextОбъект содержит два компонента,<Provider />а также<Consumer />.

class App extends React.Component {
  render () {
    return (
      <ThemeContext.Provider value={{background: 'green', color: 'white'}}>
        <Header />
      </ThemeContext.Provider>
    );
  }
}

<Provider />изvalueэквивалентно настоящемуgetChildContext().

class Header extends React.Component {
  render () {
    return (
      <Title>Hello React Context API</Title>
    );
  }
}
 
class Title extends React.Component {
  render () {
    return (
      <ThemeContext.Consumer>
        {context => (
          <h1 style={{background: context.background, color: context.color}}>
            {this.props.children}
          </h1>
        )}
      </ThemeContext.Consumer>
    );
  }
}

<Consumer />изchildrenДолжна быть функция, полученная через параметры функции<Provider />который предоставилContext.

видимый,ContextНовый API ближе к стилю React.

Несколько мест, где контекст можно получить напрямую

На самом деле, в дополнение к экземпляруcontextАтрибуты(this.context), есть много мест, где компоненты React могут напрямую обращаться кContext. Например, метод построения:

  • constructor(props, context)

Например, жизненный цикл:

  • componentWillReceiveProps(nextProps, nextContext)
  • shouldComponentUpdate(nextProps, nextState, nextContext)
  • componetWillUpdate(nextProps, nextState, nextContext)

Для функционально-ориентированных компонентов без сохранения состояния вы можете напрямую обращаться кContext.

const StatelessComponent = (props, context) => (
  ......
)

ВышеContextОснову, более конкретное содержание руководства можно найти вздесь

Мое понимание контекста

Хорошо, после основ, давайте поговорим о моем ReactContextпонимание.

Рассматривать контекст как область действия компонента

Разработчики, использующие React, знают, что приложение React — это, по сути, дерево компонентов React.Каждый компонент React эквивалентен узлу в этом дереве.За исключением корневого узла приложения, каждый другой узел имеет цепочку родительских компонентов.

Например, картинка выше,<Child />Цепочка родительских компонентов<SubNode /> -- <Node /> -- <App />,<SubNode />Цепочка родительских компонентов<Node /> -- <App />,<Node />Цепочка родительских компонентов имеет только один компонентный узел, который<App />.

Эти соединенные деревом узлы компонентов фактически образуютContextдерево, каждый узелContext, от всех узлов компонентов в цепочке родительских компонентов черезgetChildContext()При условииContextОбъект, состоящий из объектов.

Разработчики, понимающие концепцию цепочки областей действия JS, должны знать, что во время выполнения блока кода JS будет создана соответствующая цепочка областей действия, в которой записываются активные объекты, к которым можно получить доступ во время выполнения блока кода JS во время выполнения. ., включая переменные и функции, программы JS получают доступ к переменным и функциям внутри или вне блока кода через цепочку областей видимости.

Если в качестве аналогии используется цепочка областей видимости JS, компоненты React обеспечиваютContextОбъект на самом деле похож на область, доступную для дочерних компонентов, иContextСвойства объекта можно рассматривать как активные объекты в области видимости. Из-за компонентаContextПередано всеми компонентами в родительской цепочкеgetChildContext()вернутьContextОбъекты составлены, поэтому компоненты проходят черезContextОн предоставляется всеми компонентами узла в цепочке родительских компонентов, к которым можно получить доступ.Contextхарактеристики.

Итак, я позаимствовал идею JS scope chain, поставилContextкак будтообъем компонентаиспользовать.

Сосредоточьтесь на управляемости и объеме контекста.

Однако, рассматриваемый как область действия компонентаContextЭто отличается от общепринятой концепции области действия (в языках программирования, с которыми я сейчас сталкиваюсь). нам нужно обратить вниманиеContextуправляемость и радиус действия.

В нашей обычной разработке использование области действия или контекста очень распространено, естественно и даже незаметно.ContextЭто не так просто. родительский компонент предоставляетContextнужно пройтиchildContextTypesСделайте «декларацию», дочерний компонент использует родительский компонентContextсвойства должны пройтиcontextTypesсделать «применить», поэтому я думаю, что ReactContextЯвляется "разрешенной" областью действия компонента.

Каковы преимущества этого «разрешенного» подхода? Насколько я понимаю, во-первых, необходимо поддерживать согласованность API фреймворка, аpropTypesОпять же, используйте декларативный стиль кодирования. Кроме того,В определенной степени можно гарантировать, что компоненты, предоставляемыеContextуправляемость и досягаемость.

Компоненты приложения React представляют собой древовидные структуры, расширяющиеся слой за слоем, а компоненты «родитель-потомок» представляют собой линейные зависимости «один ко многим». использовать по желаниюContextНа самом деле эта зависимость будет уничтожена, что приведет к некоторым ненужным дополнительным зависимостям между компонентами, что уменьшит возможность повторного использования компонентов, что может повлиять на ремонтопригодность приложения.

Как видно из приведенного выше рисунка, дерево компонентов, которое изначально было линейно зависимым, поскольку дочерний компонент использует родительский компонентContext, Привести к<Child />пара компонентов<Node />а также<App />У всех есть зависимости. После отделения от этих двух компонентов<Child />Наличие<Child />возможности повторного использования.

я считаю,пройти черезContextПредоставление данных или API не является элегантной практикой., несмотря на то, что делает react-redux. Следовательно, необходим механизм или ограничение для уменьшения ненужных эффектов.

пройти черезchildContextTypesа такжеcontextTypesОграничения этих двух статических свойств могут быть гарантированы в определенной степени.Только сам компонент или другие подкомпоненты, связанные с компонентом, могут быть доступны по желанию.Contextсвойства, либо данные, либо функции. Потому что только сам компонент или связанные с ним подкомпоненты могут знать, что он может получить доступContextКакие свойства относительно других компонентов, которые не связаны с компонентом, будь то внутренние или внешние, потому что неясно о родительских компонентах в цепочке родительских компонентовchildContextTypesЧто "заявлено"Contextатрибут, поэтому он не может пройтиcontextTypes«Применить» связанные свойства. Итак, я понимаю, что область действия, заданная компонентуContext"С разрешения" можно в какой-то мере обеспечитьContextуправляемость и радиус действия.

В процессе разработки компонентов мы всегда должны обращать на это внимание, а не использовать это по своему усмотрению.Context.

Не нужно сначала использовать Context

Поскольку высокоуровневый API React, React иНе рекомендуется, мы отдаем приоритет использованиюContext. Мое понимание:

  • ContextОн все еще находится в экспериментальной стадии, и в более поздних версиях релиза могут быть серьезные изменения.На самом деле такая ситуация уже произошла.Поэтому, чтобы избежать большего влияния и проблем при будущих обновлениях, не рекомендуется его использовать в приложении.Context.
  • Хотя это не рекомендуется для использования в приложенияхContext, но для компонентов, поскольку сфера влияния меньше, чем у приложения, если вы можете достичь высокой связности и не разрушить зависимости дерева компонентов, вы все равно можете рассмотреть возможность использованияContextиз.
  • Для обмена данными или управления состоянием между компонентами предпочтительно использоватьpropsилиstateРешите, а затем рассмотрите возможность использования других сторонних зрелых библиотек для решения, когда вышеуказанные методы не являются лучшим выбором, рассмотрите возможность использованияContext.
  • ContextОбновление должно пройтиsetState()триггер, но это ненадежно.ContextПоддержка кросс-сборок, однако, если промежуточные подкомпоненты не обновляются некоторыми методами, такими какshouldComponentUpdate()вернутьfalse, то нет гарантииContextОбновление должно быть доступно для использованияContextподкомпонент . следовательно,Contextнадежность требует внимания. Однако обновленная проблема была решена в новой версии API.

Короче говоря, пока вы можете обеспечитьContextможно контролировать, используяContextСерьезной проблемы нет, даже если ее можно разумно применить,ContextНа самом деле, это может дать очень мощный опыт в разработке компонентов React.

Используйте Context как средство для обмена данными

Официально упоминаетсяContextМожет использоваться для передачи данных между компонентами. И я, понимаю, как мост, как средаобмен данными. Обмен данными можно разделить на две категории:Уровень приложенияа такжекомпонентный уровень.

  • Обмен данными на уровне приложения

Предоставляется компонентом корневого узла приложения.ContextОбъект можно рассматривать как глобальную область действия уровня приложения, поэтому мы используем компонент корневого узла приложения, чтобы предоставитьContextОбъект создает некоторые глобальные данные на уровне приложения. Готовые примеры могут относиться к react-redux, ниже приведены<Provider />Основная реализация исходного кода компонента:

export function createProvider(storeKey = 'store', subKey) {
    const subscriptionKey = subKey || `${storeKey}Subscription`

    class Provider extends Component {
        getChildContext() {
          return { [storeKey]: this[storeKey], [subscriptionKey]: null }
        }

        constructor(props, context) {
          super(props, context)
          this[storeKey] = props.store;
        }

        render() {
          return Children.only(this.props.children)
        }
    }

    // ......

    Provider.propTypes = {
        store: storeShape.isRequired,
        children: PropTypes.element.isRequired,
    }
    Provider.childContextTypes = {
        [storeKey]: storeShape.isRequired,
        [subscriptionKey]: subscriptionShape,
    }

    return Provider
}

export default createProvider()

Корневой компонент приложения<Provider />После того, как компонент упакован, он, по сути, предоставляет глобальное свойство для приложения.store, что эквивалентно совместному использованию во всем приложенииstoreАтрибуты. Конечно,<Provider />Компоненты также могут быть заключены в другие компоненты и совместно использоваться глобально на уровне компонентов.store.

  • Обмен данными на уровне компонентов

Если функция компонента не может быть выполнена самим компонентом, но также необходимо полагаться на дополнительные подкомпоненты, то вы можете использоватьContextСоздайте компонент, состоящий из нескольких подкомпонентов. Например, реактивный маршрутизатор.

реактивный маршрутизатор<Router />Он не может выполнять операции и управление маршрутизацией самостоятельно, поскольку содержание навигационных ссылок и переходов обычно разнесено, поэтому ему также необходимо полагаться на<Link />а также<Route />Осадочные компоненты для завершения сопутствующих работ по маршруту. Чтобы соответствующие подкомпоненты работали вместе, используется реализация React-Router.Contextсуществует<Router />,<Link />так же как<Route />Совместно между этими связанными компонентамиrouter, а затем завершите унифицированную работу и управление маршрутизацией.

Взято ниже<Router />,<Link />так же как<Route />Эти связанные компоненты являются частью исходного кода для лучшего понимания вышеизложенного.

// Router.js

/**
 * The public API for putting history on context.
 */
class Router extends React.Component {
  static propTypes = {
    history: PropTypes.object.isRequired,
    children: PropTypes.node
  };

  static contextTypes = {
    router: PropTypes.object
  };

  static childContextTypes = {
    router: PropTypes.object.isRequired
  };

  getChildContext() {
    return {
      router: {
        ...this.context.router,
        history: this.props.history,
        route: {
          location: this.props.history.location,
          match: this.state.match
        }
      }
    };
  }
  
  // ......
  
  componentWillMount() {
    const { children, history } = this.props;
    
    // ......
    
    this.unlisten = history.listen(() => {
      this.setState({
        match: this.computeMatch(history.location.pathname)
      });
    });
  }

  // ......
}

Хотя исходный код имеет другую логику, но<Router />Суть в том, чтобы предоставить дочернему компонентуrouterатрибутContext, при мониторингеhistory,однаждыhistoryизменение, черезsetState()Запустите компонент для повторного рендеринга.

// Link.js

/**
 * The public API for rendering a history-aware <a>.
 */
class Link extends React.Component {
  
  // ......
  
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        push: PropTypes.func.isRequired,
        replace: PropTypes.func.isRequired,
        createHref: PropTypes.func.isRequired
      }).isRequired
    }).isRequired
  };

  handleClick = event => {
    if (this.props.onClick) this.props.onClick(event);

    if (
      !event.defaultPrevented &&
      event.button === 0 &&
      !this.props.target &&
      !isModifiedEvent(event)
    ) {
      event.preventDefault();
      // 使用<Router />组件提供的router实例
      const { history } = this.context.router;
      const { replace, to } = this.props;

      if (replace) {
        history.replace(to);
      } else {
        history.push(to);
      }
    }
  };
  
  render() {
    const { replace, to, innerRef, ...props } = this.props;

    // ...

    const { history } = this.context.router;
    const location =
      typeof to === "string"
        ? createLocation(to, null, null, history.location)
        : to;

    const href = history.createHref(location);
    return (
      <a {...props} onClick={this.handleClick} href={href} ref={innerRef} />
    );
  }
}

<Link />Ядро рендеринга<a>метка, перехват<a>событие щелчка метки, а затем передать<Router />общийrouterправильноhistoryВыполнение операций маршрутизации для уведомления<Router />Перерендерить.

// Route.js

/**
 * The public API for matching a single path and rendering.
 */
class Route extends React.Component {
  
  // ......
  
  state = {
    match: this.computeMatch(this.props, this.context.router)
  };

  // 计算匹配的路径,匹配的话,会返回一个匹配对象,否则返回null
  computeMatch(
    { computedMatch, location, path, strict, exact, sensitive },
    router
  ) {
    if (computedMatch) return computedMatch;
    
    // ......

    const { route } = router;
    const pathname = (location || route.location).pathname;
    
    return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
  }
 
  // ......

  render() {
    const { match } = this.state;
    const { children, component, render } = this.props;
    const { history, route, staticContext } = this.context.router;
    const location = this.props.location || route.location;
    const props = { match, location, history, staticContext };

    if (component) return match ? React.createElement(component, props) : null;

    if (render) return match ? render(props) : null;

    if (typeof children === "function") return children(props);

    if (children && !isEmptyChildren(children))
      return React.Children.only(children);

    return null;
  }
}

<Route />Есть часть исходного кода с<Router />Аналогично, вложенность маршрутов может быть достигнута, но суть в том, чтобы пройтиContextобщийrouter, определите, соответствует ли он пути текущего маршрута, а затем визуализируйте компонент.

Из приведенного выше анализа видно, что весь реагирующий маршрутизатор на самом деле вращается вокруг<Router />изContextстроить.

Используйте Context для разработки компонентов

раньше, черезContextРазработал простой компонент — компонент распределения слотов. В этой главе мы поговорим о том, как использовать опыт разработки этого компонента распределения слотов.ContextРазрабатывать компоненты.

компоненты распределения слотов

Прежде всего, что такое компонент диспетчеризации слотов, эта концепция впервые была признана в Vuejs. Распределение слотов — это технология, которая вставляет содержимое родительского компонента в шаблон дочернего компонента через композицию компонентов, которая вызывается в Vuejs.Slot.

Чтобы все могли понять эту концепцию более интуитивно, я привёл демонстрацию о распределении слотов от Vuejs.

компоненты для предусмотренных слотов<my-component />, шаблон выглядит следующим образом:

<div>
  <h2>我是子组件的标题</h2>
  <slot>
    只有在没有要分发的内容时显示
  </slot>
</div>

Для родительского компонента шаблон выглядит следующим образом:

<div>
  <h1>我是父组件的标题</h1>
  <my-component>
    <p>这是一些初始内容</p>
    <p>这是更多的初始内容</p>
  </my-component>
</div>

Окончательный результат визуализации:

<div>
  <h1>我是父组件的标题</h1>
  <div>
    <h2>我是子组件的标题</h2>
    <p>这是一些初始内容</p>
    <p>这是更多的初始内容</p>
  </div>
</div>

компоненты можно увидеть<my-component />из<slot />Узел в конечном итоге помещается в родительский компонент<my-component />Заменено содержимым под узлом.

Vuejs также поддерживаетименованный слот.

Например, компонент макета<app-layout />:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

И в шаблоне родительского компонента:

<app-layout>
  <h1 slot="header">这里可能是一个页面标题</h1>
  <p>主要内容的一个段落。</p>
  <p>另一个段落。</p>
  <p slot="footer">这里有一些联系信息</p>
</app-layout>

Окончательный результат визуализации:

<div class="container">
  <header>
    <h1>这里可能是一个页面标题</h1>
  </header>
  <main>
    <p>主要内容的一个段落。</p>
    <p>另一个段落。</p>
  </main>
  <footer>
    <p>这里有一些联系信息</p>
  </footer>
</div>

Преимущество распределения слотов заключается в том, что оно позволяет абстрагировать компоненты в шаблоны. Сам компонент заботится только о структуре шаблона, а конкретный контент передается родительскому компоненту для обработки, в то же время он не нарушает грамматическое выражение HTML для описания структуры DOM. Я думаю, что это очень значимая технология, к сожалению, React поддерживает эту технологию не так дружелюбно. Поэтому я сослался на компонент распределения слотов Vuejs и разработал набор компонентов распределения слотов на основе React, которые могут сделать компоненты React также способными создавать шаблоны.

для<AppLayout />компонента, я надеюсь, что это можно записать следующим образом:

class AppLayout extends React.Component {
  static displayName = 'AppLayout'
  
  render () {
    return (
      <div class="container">
        <header>
          <Slot name="header"></Slot>
        </header>
        <main>
          <Slot></Slot>
        </main>
        <footer>
          <Slot name="footer"></Slot>
        </footer>
      </div>
    )
  }
}

При использовании во внешнем слое это можно записать так:

<AppLayout>
  <AddOn slot="header">
    <h1>这里可能是一个页面标题</h1>
  </AddOn>
  <AddOn>
    <p>主要内容的一个段落。</p>
  	<p>另一个段落。</p>
  </AddOn>
  <AddOn slot="footer">
    <p>这里有一些联系信息</p>
  </AddOn>
</AppLayout>

Идеи реализации компонентов

Согласно предыдущим мыслям, сначала организуйте реализацию идей.

Нетрудно заметить, что компонент распределения слотов должен опираться на два подкомпонента — компонент слотов<Slot />и компоненты дистрибутива<AddOn />. Слот-компонент отвечает за нагромождение и предоставление ямы для раздачи контента. Компонент распространения отвечает за сбор контента распространения и предоставление его компоненту слота для воспроизведения контента распространения, что эквивалентно потребителю слота.

Очевидно, здесь есть проблема,<Slot />компоненты с<AddOn />компоненты независимы, как<AddOn />Контент для заполнения<Slot />в? Решить эту проблему несложно, два независимых модуля должны установить соединение, поэтому постройте для них мост. Так как построить этот мост? Давайте вернемся и посмотрим на код, который мы представили ранее.

для<AppLayout />компонент, я хочу написать его следующим образом:

class AppLayout extends React.Component {
  static displayName = 'AppLayout'
  
  render () {
    return (
      <div class="container">
        <header>
          <Slot name="header"></Slot>
        </header>
        <main>
          <Slot></Slot>
        </main>
        <footer>
          <Slot name="footer"></Slot>
        </footer>
      </div>
    )
  }
}

При использовании во внешнем слое напишите это так:

<AppLayout>
  <AddOn slot="header">
    <h1>这里可能是一个页面标题</h1>
  </AddOn>
  <AddOn>
    <p>主要内容的一个段落。</p>
  	<p>另一个段落。</p>
  </AddOn>
  <AddOn slot="footer">
    <p>这里有一些联系信息</p>
  </AddOn>
</AppLayout>

Будь то<Slot />еще<AddOn />, на самом деле все<AppLayout />в рамках.<Slot />да<AppLayout />компонентыrender()узел компонента, возвращаемый методом, в то время как<AddOn />является<AppLayout />изchildrenузел, поэтому мы можем<AppLayout />рассматривается как<Slot />а также<AddOn />роль моста. Так,<AppLayout />по чем подарить<Slot />а также<AddOn />Как насчет подключения? Вот главный герой этой статьи -Context. Следующий вопрос, как использоватьContextДать<Slot />а также<AddOn />строить связь?

упомянутый ранее<AppLayout />этот мост. Во внешнем компоненте<AppLayout />ответственный за прохождение<AddOn />Соберите то, что заполняет слот.<AppLayout />самопомощьContextОпределите интерфейс, который получает население. При рендеринге, потому что<Slot />да<AppLayout />визуализированный узел, поэтому,<Slot />в состоянии пройтиContextполучать<AppLayout />Определенный интерфейс для получения содержимого заполнения, а затем через этот интерфейс содержимое заполнения получается для рендеринга.

Реализовать компонент распределения слотов по задумке

из-за<AddOn />да<AppLayout />изchildrenузел, и<AddOn />это конкретный компонент, мы можем передатьnameилиdisplayNameвыявлено, значит,<AppLayout />Перед рендерингом, т.е.render()изreturnраньше, даchildrenпройти кslotзначение какkey, поставить каждый<AddOn />изchildrenкэш вниз. если<AddOn />нет настройкиslot, а затем рассматривать его как предоставление неназванного<Slot />Заполните контент, мы можем назначить один из этих безымянных слотовkey, например, вызов?default.

для<AppLayout />, код примерно такой:

class AppLayout extends React.Component {
  
  static childContextTypes = {
    requestAddOnRenderer: PropTypes.func
  }
  
  // 用于缓存每个<AddOn />的内容
  addOnRenderers = {}
  
  // 通过Context为子节点提供接口
  getChildContext () {
    const requestAddOnRenderer = (name) => {
      if (!this.addOnRenderers[name]) {
        return undefined
      }
      return () => (
        this.addOnRenderers[name]
      )
    }
    return {
      requestAddOnRenderer
    }
  }

  render () {
    const {
      children,
      ...restProps
    } = this.props

    if (children) {
      // 以k-v的方式缓存<AddOn />的内容
      const arr = React.Children.toArray(children)
      const nameChecked = []
      this.addOnRenderers = {}
      arr.forEach(item => {
        const itemType = item.type
        if (item.type.displayName === 'AddOn') {
          const slotName = item.props.slot || '?default'
          // 确保内容唯一性
          if (nameChecked.findIndex(item => item === stubName) !== -1) {
            throw new Error(`Slot(${slotName}) has been occupied`)
          }
          this.addOnRenderers[stubName] = item.props.children
          nameChecked.push(stubName)
        }
      })
    }

    return (
      <div class="container">
        <header>
          <Slot name="header"></Slot>
        </header>
        <main>
          <Slot></Slot>
        </main>
        <footer>
          <Slot name="footer"></Slot>
        </footer>
      </div>
    )
  }
}

<AppLayout />определяетContextинтерфейсrequestAddOnRenderer(),requestAddOnRenderer()интерфейс в соответствии сnameВозвращает функцию, которая возвращает функцию на основеnameдоступaddOnRenderersхарактеристики,addOnRenderersто есть<AddOn />Объект Cache Cache.

<Slot />Реализация очень проста, следующим образом:

//            props,              context
const Slot = ({ name, children }, { requestAddOnRenderer }) => {
  const addOnRenderer = requestAddOnRenderer(name)
  return (addOnRenderer && addOnRenderer()) ||
    children ||
    null
}

Slot.displayName = 'Slot'
Slot.contextTypes = { requestAddOnRenderer: PropTypes.func }
Slot.propTypes = { name: PropTypes.string }
Slot.defaultProps = { name: '?default' }

можно увидеть<Slot />пройти черезcontextполучать<AppLayout />предоставленный интерфейсrequestAddOnRenderer(), основной объект финальной отрисовки кэшируется в<AppLayout />середина<AddOn />Содержание. Если указанный<AddOn />содержимое, а затем визуализировать<Slot />собственныйchildren.

<AddOn />проще:

const AddOn = () => null

AddOn.propTypes = { slot: PropTypes.string }
AddOn.defaultTypes = { slot: '?default' }
AddOn.displayName = 'AddOn'

<AddOn />ничего не делать, просто вернутьсяnull, его эффект заключается в том, чтобы позволить<AppLayout />Кэшировать содержимое, распределенное по слотам.

может позволить<AppLayout />более общий

Через приведенный выше код, в основном<AppLayout />Превратился в компонент с возможностью распределения слотов, но, очевидно,<AppLayout />Он не универсален, мы можем продвигать его в самостоятельный и универсальный компонент.

Я назвал этот компонент SlotProvider.

function getDisplayName (component) {
  return component.displayName || component.name || 'component'
}

const slotProviderHoC = (WrappedComponent) => {
  return class extends React.Component {
    static displayName = `SlotProvider(${getDisplayName(WrappedComponent)})`

    static childContextTypes = {
      requestAddOnRenderer: PropTypes.func
    }
  
    // 用于缓存每个<AddOn />的内容
    addOnRenderers = {}
  
    // 通过Context为子节点提供接口
    getChildContext () {
      const requestAddOnRenderer = (name) => {
        if (!this.addOnRenderers[name]) {
          return undefined
        }
        return () => (
          this.addOnRenderers[name]
        )
      }
      return {
        requestAddOnRenderer
      }
    }

    render () {
      const {
        children,
        ...restProps
      } = this.props

      if (children) {
        // 以k-v的方式缓存<AddOn />的内容
        const arr = React.Children.toArray(children)
        const nameChecked = []
        this.addOnRenderers = {}
        arr.forEach(item => {
          const itemType = item.type
          if (item.type.displayName === 'AddOn') {
            const slotName = item.props.slot || '?default'
            // 确保内容唯一性
            if (nameChecked.findIndex(item => item === stubName) !== -1) {
              throw new Error(`Slot(${slotName}) has been occupied`)
            }
            this.addOnRenderers[stubName] = item.props.children
            nameChecked.push(stubName)
          }
        })
      }
      
      return (<WrappedComponent {...restProps} />)
    }
  }
}

export const SlotProvider = slotProviderHoC

Используйте компоненты высшего порядка React для<AppLayout />Модернизируйте и превратите его в единый универсальный компонент. для оригинала<AppLayout />, вы можете использовать этоSlotProviderКомпонент более высокого порядка, преобразованный в компонент с возможностями диспетчеризации слотов.

import { SlotProvider } from './SlotProvider.js'

class AppLayout extends React.Component {
  static displayName = 'AppLayout'
  
  render () {
    return (
      <div class="container">
        <header>
          <Slot name="header"></Slot>
        </header>
        <main>
          <Slot></Slot>
        </main>
        <footer>
          <Slot name="footer"></Slot>
        </footer>
      </div>
    )
  }
}

export default SlotProvider(AppLayout)

Из приведенного выше опыта мы можем видеть, что при проектировании и разработке компонента

  • Компонентам может потребоваться корневой компонент и несколько дочерних компонентов для совместной работы для выполнения функции компонента.. Например, компонент распределения слотов действительно нуждаетсяSlotProviderа также<Slot />а также<AddOn />используется вместе,SlotProviderкак корневой компонент, а<Slot />а также<AddOn />Все являются подкомпонентами.
  • Положение дочерних компонентов относительно корневого компонента или между дочерними компонентами не определено. дляSlotProviderС точки зрения,<Slot />положение неопределенное, оно будет находиться вSlotProviderгде-нибудь в шаблоне компонента этот компонент более высокого порядка обертывает, и для<Slot />а также<AddOn />, их прямое местонахождение также неизвестно, один находится вSlotProviderвнутри упакованного компонента находится другойSlotProviderизchildren.
  • Подкомпоненты должны полагаться на какой-то глобальный API или данные.,Например<Slot />Фактическое отображаемое содержимое исходит изSlotProviderсобрал<AddOn />Содержание.

В настоящее время нам нужно использовать посредника в качестве среды для обмена данными.По сравнению с дополнительным введением сторонних модулей, таких как redux, мы можем напрямую использоватьContextМог бы быть более элегантным.

Попробуйте новую версию Context API

Модернизация предыдущего диспетчера слотов с новой версией Context API.

// SlotProvider.js

function getDisplayName (component) {
  return component.displayName || component.name || 'component'
}

export const SlotContext = React.createContext({
  requestAddOnRenderer: () => {}
})

const slotProviderHoC = (WrappedComponent) => {
  return class extends React.Component {
    static displayName = `SlotProvider(${getDisplayName(WrappedComponent)})`

    // 用于缓存每个<AddOn />的内容
    addOnRenderers = {}
  
    requestAddOnRenderer = (name) => {
      if (!this.addOnRenderers[name]) {
        return undefined
      }
      return () => (
        this.addOnRenderers[name]
      )
    }

    render () {
      const {
        children,
        ...restProps
      } = this.props

      if (children) {
        // 以k-v的方式缓存<AddOn />的内容
        const arr = React.Children.toArray(children)
        const nameChecked = []
        this.addOnRenderers = {}
        arr.forEach(item => {
          const itemType = item.type
          if (item.type.displayName === 'AddOn') {
            const slotName = item.props.slot || '?default'
            // 确保内容唯一性
            if (nameChecked.findIndex(item => item === stubName) !== -1) {
              throw new Error(`Slot(${slotName}) has been occupied`)
            }
            this.addOnRenderers[stubName] = item.props.children
            nameChecked.push(stubName)
          }
        })
      }
      
      return (
        <SlotContext.Provider value={
            requestAddOnRenderer: this.requestAddOnRenderer
          }>
          <WrappedComponent {...restProps} />
        </SlotContext.Provider>
      )
    }
  }
}

export const SlotProvider = slotProviderHoC

удалил предыдущийchildContextTypesа такжеgetChildContext(), за исключением локальных корректировок, в общем ядре серьезных изменений нет.

// Slot.js

import { SlotContext } from './SlotProvider.js'

const Slot = ({ name, children }) => {
  return (
    <SlotContext.Consumer>
      {(context) => {
        const addOnRenderer = requestAddOnRenderer(name)
          return (addOnRenderer && addOnRenderer()) ||
            children ||
            null
      }}
    </SlotContext.Consumer>
  )
}

Slot.displayName = 'Slot'
Slot.propTypes = { name: PropTypes.string }
Slot.defaultProps = { name: '?default' }

Поскольку раньше он использовался в соответствии с моделью производитель-потребитель.ContextИ сам компонент относительно прост, поэтому после использования новой API для преобразования не так много различий.

Суммировать

  • в сравнении сpropsа такжеstate, РеагироватьContextКомпонентная связь между уровнями может быть достигнута.
  • Использование Context API основано на схеме производитель-потребитель. На стороне производителя через статические свойства компонентаchildContextTypesобъявление, затем через метод экземпляраgetChildContext()СоздайтеContextобъект. Со стороны потребителя через статические свойства компонентовcontextTypesдля примененияContextсвойства, а затем передать экземплярcontextдоступContextхарактеристики.
  • использоватьContextНужно больше думать, не рекомендуется в приложенииContext, но если связность компонента может быть обеспечена в процессе разработки компонента, управляема и ремонтопригодна, без разрушения зависимостей дерева компонентов, а сфера влияния невелика, можно рассмотреть возможность использованияContextрешить некоторые проблемы.
  • пройти черезContextПредоставление доступа к API может в определенной степени облегчить решение некоторых проблем, но я лично считаю, что это не очень хорошая практика, и с ней нужно быть осторожным.
  • старая версияContextДля обновления требуются зависимостиsetState(), ненадежен, но эта проблема фиксируется в новой версии API.
  • можно поставитьContextОтноситесь к этому как к области действия компонента, но нужно обратить вниманиеContextПрежде чем использовать его, проанализируйте, действительно ли необходимо его использовать, чтобы избежать некоторых побочных эффектов, вызванных чрезмерным использованием.
  • можно поставитьContextВ качестве средства обмена данными на уровне приложения или компонента.
  • Спроектируйте и разработайте компонент. Если этот компонент необходимо объединить с несколькими компонентами, используйтеContextМожет быть, более элегантно.

Выше это то, что я поделился.Если есть какие-то недостатки или ошибки, пожалуйста, покритикуйте и исправьте их.

Цитировать

  • Context - https://reactjs.org/docs/context.html
  • React 16.3 уже здесь: с новым Context API — http://cnodejs.org/topic/5a7bd5c4497a08f571384f03
  • Content Distribution with Slots - https://vuejs.org/v2/guide/components.html#Content-Distribution-with-Slots
  • Карта названия - http://www.mafengwo.cn/poi/36580.html