Как элегантно проектировать компоненты React

JavaScript React.js Web Components
Как элегантно проектировать компоненты React

Автор: Николас Данная статья является оригинальной, просьба указывать автора и источник для перепечатки

Сегодняшний веб-интерфейс разделен на три части: React, Vue и Angular.JQuery, который доминирует в стране более десяти лет, очевидно, с трудом соответствует текущей модели разработки. Так почему же люди думают, что jQuery «устарел»? Во-первых, статья "Никакого JQuery! Нативный JavaScript для управления DOMскажу вам прямо, теперь очень удобно манипулировать DOM с помощью нативного JavaScript. Во-вторых, удобство jQuery основано на предпосылке наличия базовой структуры DOM, которая, кажется, соответствует разделению стиля, поведения и структуры, но на самом деле структура DOM и логика кода JavaScript связаны между собой. и ваши идеи разработки по-прежнему будут заключаться в переключении между структурой DOM и JavaScript.

Хотя jQuery уже не так популярен, дизайнерские идеи jQuery по-прежнему заслуживают уважения и изучения, особенно подключаемый модуль jQuery. Если вы разрабатывали подключаемые модули jQuery, вы должны знать, что подключаемый модуль должен быть достаточно гибким и иметь детальный параметрический дизайн. Гибкий и простой в использовании компонент React, такой как плагин jQuery, неотделим от разумных атрибутов (props), но разделение и композиция компонентов React на удивление просты по сравнению с плагинами jQuery.

Итак, теперь давайте возьмем универсальный TODO LIST в качестве примера для разработки React.TodoListкомпоненты!

Реализовать основные функции

Предположительно, мы все должны знать функцию TODO LIST, то есть добавление, удаление, изменение и т. д. TODO. Сама функция относительно проста, чтобы не усложнять пример, мы не будем расширять функцию отображения навигации (все, выполнено, неполное) TODO LIST в разных состояниях.

конвенционная структура каталогов

Предположим, у нас уже есть скаффолдинг, который может запускать проект React (ха~, потому что я здесь не для того, чтобы учить вас, как строить скаффолдинг), тогда каталог с исходным кодом проектаsrc/Следующее может выглядеть так:

.
├── components
├── containers
│   └── App
│       ├── app.scss
│       └── index.js
├── index.html
└── index.js

Давайте сначала кратко объясним настройку этого каталога. Видим в корневом каталогеindex.jsФайл является входным модулем всего проекта, входной модуль будет обрабатывать рендеринг DOM и горячее обновление компонентов React (react-hot-loader) и другие настройки. Потом,index.htmlЭто файл HTML-шаблона страницы.Эти две части сейчас не в центре нашего внимания, и мы не будем их обсуждать дальше.

входной модульindex.jsКод выглядит следующим образом:

// import reset css, base css...

import React from 'react';
import ReactDom from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import App from 'containers/App';

const render = (Component) => {
  ReactDom.render(
    <AppContainer>
      <Component />
    </AppContainer>,
    document.getElementById('app')
  );
};

render(App);

if (module.hot) {
  module.hot.accept('containers/App', () => {
    let nextApp = require('containers/App').default;
    
    render(nextApp);
  });
}

см. далееcontainers/каталог, в нем будут размещены компоненты контейнера нашей страницы, бизнес-логика, обработка данных и т. д., которые будут обрабатываться на этом уровне,containers/AppБудет основным компонентом-контейнером для нашей страницы. В качестве общих компонентов мы помещаем их вcomponents/Под содержанием.

Базовая структура каталогов кажется завершенной, давайте реализуем основной компонент контейнера.containers/App.

Реализовать основной контейнер

Давайте сначала посмотрим на основной компонент контейнераcontainers/App/index.jsИсходная реализация кода:

import React, { Component } from 'react';
import styles from './app.scss';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      todos: []
    };
  }

  render() {
    return (
      <div className={styles.container}>
        <h2 className={styles.header}>Todo List Demo</h2>
        <div className={styles.content}>
          <header className={styles['todo-list-header']}>
            <input 
              type="text"
              className={styles.input}
              ref={(input) => this.input = input} 
            />
            <button 
              className={styles.button} 
              onClick={() => this.handleAdd()}
            >
              Add Todo
            </button>
          </header>
          <section className={styles['todo-list-content']}>
            <ul className={styles['todo-list-items']}>
              {this.state.todos.map((todo, i) => (
                <li key={`${todo.text}-${i}`}>
                  <em 
                    className={todo.completed ? styles.completed : ''} 
                    onClick={() => this.handleStateChange(i)}
                  >
                    {todo.text}
                  </em>
                  <button 
                    className={styles.button} 
                    onClick={() => this.handleRemove(i)}
                  >
                    Remove
                  </button>
                </li>
              ))}
            </ul>
          </section>
        </div>
      </div>
    );
  }

  handleAdd() {
    ...
  }

  handleRemove(index) {
    ...
  }

  handleStateChange(index) {
    ...
  }
}

export default App;

Мы можем запихнуть всю бизнес-логику в основной контейнер, как указано выше, но мы должны учитывать, что основной контейнер будет собирать другие компоненты в любое время, складывая различные логики вместе, тогда этот компонент станет несравнимым Огромным, пока не «выйдет из-под контроля». ". Итак, мы должны выделить независимуюTodoListкомпоненты.

отдельные компоненты

Компонент TodoList

существуетcomponents/В каталоге создаем новыйTodoListПапки и связанные файлы:

.
├── components
+│   └── TodoList
+│       ├── index.js
+│       └── todo-list.scss
├── containers
│   └── App
│       ├── app.scss
│       └── index.js
...

Тогда мы будемcontainers/App/index.jsкаблукTodoListФункции, связанные с компонентами, извлечены вcomponents/TodoList/index.jsсередина:

...
import styles from './todo-list.scss';

export default class TodoList extends Component {
  ...
  
  render() {
    return (
      <div className={styles.container}>
-       <header className={styles['todo-list-header']}>
+       <header className={styles.header}>
          <input 
            type="text"
            className={styles.input}
            ref={(input) => this.input = input} 
          />
          <button 
            className={styles.button} 
            onClick={() => this.handleAdd()}
          >
            Add Todo
          </button>
        </header>
-       <section className={styles['todo-list-content']}>
+       <section className={styles.content}>
-         <ul className={styles['todo-list-items']}>
+         <ul className={styles.items}>
            {this.state.todos.map((todo, i) => (
              <li key={`${todo}-${i}`}>
                <em 
                  className={todo.completed ? styles.completed : ''} 
                  onClick={() => this.handleStateChange(i)}
                >
                  {todo.text}
                </em>
                <button 
                  className={styles.button} 
                  onClick={() => this.handleRemove(i)}
                >
                  Remove
                </button>
              </li>
            ))}
          </ul>
        </section>
      </div>
    );
  }

  ...
}

Вы заметили вышеrenderв методеclassName, мы опустилиtodo-list*префикс, так как мы используемCSS MODULES, поэтому, когда мы отсоединяем компонент, исходное определение в основном контейнереtodo-list*с префиксомclassName, чего можно легко добиться с помощью конфигурации веб-пакета:

...
module.exports = {
  ...
  module: {
    rules: [
      {
    	test: /\.s?css/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[name]--[local]-[hash:base64:5]'
            }
          },
          ...
        ]
      }
    ]  
  }
  ...
};

Посмотрим на результат после вывода кода этого компонента:

<div data-reactroot="" class="app--container-YwMsF">
  ...
    <div class="todo-list--container-2PARV">
      <header class="todo-list--header-3KDD3">
        ...
      </header>
      <section class="todo-list--content-3xwvR">
        <ul class="todo-list--items-1SBi6">
          ...
        </ul>
      </section>
    </div>
</div>

Как вы можете видеть из конфигурации веб-пакета и вывода HTML выше,classNameПроблема пространства имен может быть семантически*.scssМетод имени файла для достижения, напримерTodoListфайл стиляtodo-list.scss. Это избавляет нас от определения компонентовclassNameРаздражение, вызванное пространством имен, поэтому вам нужно начать только со структуры внутри компонента.

Вернемся к теме, давайте снова посмотрим на разлукуTodoListпосле компонентаcontainers/App/index.js:

import TodoList from 'components/TodoList';
...

class App extends Component {
  render() {
    return (
      <div className={styles.container}>
        <h2 className={styles.header}>Todo List Demo</h2>
        <div className={styles.content}>
          <TodoList />
        </div>
      </div>
    );
  }
}

export default App;

Извлечение общих компонентов

В качестве проекта текущийTodoListКомпонент содержит слишком много дочерних элементов, таких как: ввод, кнопка и т. д. Для компонента"Напишите один раз, используйте везде” принцип, мы можем далее разделитьTodoListкомпоненты для удовлетворения использования других компонентов.

Однако как разделить компоненты наиболее разумно? Я не думаю, что есть лучший ответ на этот вопрос, но мы можем думать об этом несколькими способами: инкапсуляция, повторное использование и гибкость. такие как взятиеh1Элементарно, вы можете инкапсулировать его какTitleкомпонент, то это<Title text={title} />использовать, или вы можете<Title>{title}</Title>использовать. Но заметили ли вы, что это достигаетсяTitleКомпоненты не играют роли упрощения и инкапсуляции, а увеличивают сложность использования.h1Это также сам компонент, поэтому нам также необходимо освоить степень, когда мы разделяем компоненты.

Хорошо, давайте начнем с ввода и кнопки, вcomponents/Создайте 2 новых в каталогеButtonа такжеInputКомпоненты:

.
├── components
+│   ├── Button
+│   │   ├── button.scss
+│   │   └── index.js
+│   ├── Input
+│   │   ├── index.js
+│   │   └── input.scss
│   └── TodoList
│       ├── index.js
│       └── todo-list.scss
...

Button/index.jsкод:

...
export default class Button extends Component {
  render() {
    const { className, children, onClick } = this.props;

    return (
      <button 
        type="button" 
        className={cn(styles.normal, className)} 
        onClick={onClick}
      >
        {children}
      </button>
    );
  }
}

Input/index.jsкод:

...
export default class Input extends Component {
  render() {
    const { className, value, inputRef } = this.props;

    return (
      <input 
        type="text"
        className={cn(styles.normal, className)}
        defaultValue={value}
        ref={inputRef} 
      />
    );
  }
}

Поскольку эти два компонента сами по себе не включают никакой бизнес-логики, они должны принадлежать чистым компонентам рендеринга (кукольным компонентам), мы можем использовать легковесные компоненты React без сохранения состояния для объявления:

...
const Button = ({ className, children, onClick }) => (
  <button 
    type="button" 
    className={cn(styles.normal, className)} 
    onClick={onClick}
  >
    {children}
  </button>
);

Вам не кажется, что это очень круто!

Кроме того, изInputКак видно из примера кода компонента, мы используемнеконтролируемые компоненты, здесь специально разработан, чтобы упростить пример кода.Вы можете решить, нужно ли вам разрабатывать его в соответствии с вашей реальной ситуацией.Управляемые компоненты. В общем, если вам не нужно получать входные значения в реальном времени, я думаю, что использования неконтролируемого компонента должно быть достаточно.

Вернемся к вышесказанномуTodoListкомпонент, который будет ранее отсоединенным подкомпонентомButton,InputСоберитесь в.

...
import Button from 'components/Button';
import Input from 'components/Input';
...

export default class TodoList extends Component {
  render() {
    return (
      <div className={styles.container}>
        <header className={styles.header}>
          <Input 
            className={styles.input} 
            inputRef={(input) => this.input = input} 
          />
          <Button onClick={() => this.handleAdd()}>
            Add Todo
          </Button>
        </header>
        ...
      </div>
    );
  }
}

...

Разделить подкомпоненты

Тогда продолжайте смотретьTodoListВ разделе элементов мы заметили, что эта часть содержит больше логики рендеринга вrender, нам нужно слишком много думать между этим кодом и контекстом, так почему бы нам не извлечь его:

...

export default class TodoList extends Component {
  render() {
    return (
      <div className={styles.container}>
        ...
        <section className={styles.content}>
          {this.renderItems()}
        </section>
      </div>
    );
  }

  renderItems() {
    return (
      <ul className={styles.items}>
        {this.state.todos.map((todo, i) => (
          <li key={`${todo}-${i}`}>
            ...
          </li>
        ))}
      </ul>
    );
  }
  
  ...
}

Приведенный выше код, кажется, уменьшаетrenderсложности, но все же не позволяетTodoListуменьшить нагрузку. Поскольку мы должны отделить эту часть логики, почему бы не создать одинTodosКомпоненты, как насчет разделения этой части логики? Итак, начнем с "Ближайшее заявление«Принципcomponents/TodoList/Создайте подкаталог в каталогеcomponents/TodoList/components/хранитьTodoListподкомпонент . Зачем? потому что я думаю, что компонентTodosа такжеTodoListОн имеет тесные родительско-дочерние отношения и не имеет никакого взаимодействия с другими компонентами.TodoListчастный.

Затем мы просматриваем текущую структуру каталогов:

.
├── components
│   ...
│   └── TodoList
+│       ├── components
+│       │   └── Todos
+│       │       ├── index.js
+│       │       └── todos.scss
│       ├── index.js
│       └── todo-list.scss

Todos/index.jsкод:

...
const Todos = ({ data: todos, onStateChange, onRemove }) => (
  <ul className={styles.items}>
    {todos.map((todo, i) => (
      <li key={`${todo}-${i}`}>
        <em 
          className={todo.completed ? styles.completed : ''} 
          onClick={() => onStateChange(i)}
        >
          {todo.text}
        </em>
        <Button onClick={() => onRemove(i)}>
          Remove
        </Button>
      </li>
    ))}
  </ul>
);
...

Посмотрите на расколTodoList/index.js:

render() {
  return (
    <div className={styles.container}>
      ...
      <section className={styles.content}>
        <Todos 
          data={this.state.todos}
          onStateChange={(index) => this.handleStateChange(index)}
          onRemove={(index) => this.handleRemove(index)}
        />
      </section>
    </div>
  );
}

Расширенный подкомпонент

На данный момент основные функции завершены, а подкомпоненты, кажется, разделены, так что функцию подкомпонента можно легко улучшить. просто возьмиTodosНапример, после добавления TODO, если мы не выполнили это TODO и надеемся изменить его содержимое. ха~ не волнуйся, или мы разделим этоTodos, например, добавлениеTodoКомпоненты:

.
├── components
│   ...
│   └── TodoList
│       ├── components
+│       │   ├── Todo
+│       │   │   ├── index.js
+│       │   │   └── todo.scss
│       │   └── Todos
│       │       ├── index.js
│       │       └── todos.scss
│       ├── index.js
│       └── todo-list.scss

Первый взглядTodosКомпоненты вытаскиваютсяTodoПосле просмотра:

...
import Todo from '../Todo';
...

const Todos = ({ data: todos, onStateChange, onRemove }) => (
  <ul className={styles.items}>
    {todos.map((todo, i) => (
      <li key={`${todo}-${i}`}>
        <Todo
          {...todo}
          onClick={() => onStateChange(i)}
        />
        <Button onClick={() => onRemove(i)}>
          Remove
        </Button>
      </li>
    ))}
  </ul>
);

export default Todos;

нам все равноTodoКак это достигается, как мы сказали выше, нам нужно понять этоTodoДобавьте редактируемую функцию, начиная с простой конфигурации свойства, нам нужно только добавитьeditableсвойства:

<Todo
  {...todo}
+ editable={editable}
  onClick={() => onStateChange(i)}
/>

Тогда давайте подумаем об этом,TodoВнутри компонента нам нужно реорганизовать некоторую функциональную логику:

  • По поступающимeditableсвойство, чтобы определить, нужно ли отображать кнопку редактирования
  • Отображать ли поле ввода текста или текстовое содержимое в соответствии с состоянием редактирования внутри компонента
  • После нажатия кнопки «Обновить» необходимо уведомить родительский компонент об обновлении списка данных.

Давайте сделаем это первымTodoПервая функциональная точка:

render() {
  const { completed, text, editable, onClick } = this.props;

  return (
    <span className={styles.wrapper}>
      <em
        className={completed ? styles.completed : ''} 
        onClick={onClick}
        >
        {text}
      </em>
      {editable && 
        <Button>
          Edit
        </Button>
      }
    </span>
  );
}

Очевидно, кажется, что для достижения этого шага luan бесполезен.Нам также нужно нажать кнопку «Изменить», чтобы отобразитьInputКомпоненты, которые делают контент изменяемым. Таким образом, простой передачи свойств недостаточно для функциональности компонента, нам также нужно внутреннее состояние, чтобы управлять тем, находится ли компонент в процессе редактирования:

render() {
  const { completed, text, editable, onStateChange } = this.props,
    { editing } = this.state;

  return (
    <span className={styles.wrapper}>
      {editing ? 
        <Input 
          value={text}
          className={styles.input}
          inputRef={input => this.input = input}
        /> :
        <em
          className={completed ? styles.completed : ''} 
          onClick={onStateChange}
        >
          {text}
        </em>
      }
      {editable && 
        <Button onClick={() => this.handleEdit()}>
          {editing ? 'Update' : 'Edit'}
        </Button>
      }
    </span>
  );
}

наконец,TodoКомпонент должен уведомить родительский компонент об обновлении данных после нажатия кнопки «Обновить»:

handleEdit() {
  const { text, onUpdate } = this.props;
  let { editing } = this.state;

  editing = !editing;

  this.setState({ editing });

  if (!editing && this.input.value !== text) {
    onUpdate(this.input.value);
  }
}

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

Вернемся и изменимTodosпара компонентовTodoвызов. сначала добавьтеTodoListСвойство обратного вызова, переданное из компонентаonUpdate, при измененииonClickдляonStateChange, потому что в это времяTodoСуществует больше, чем просто одно событие щелчка, и вам необходимо определить обратные вызовы событий при изменении различных состояний:

<Todo
  {...todo}
  editable={editable}
- onClick={() => onStateChange(i)}
+ onStateChange={() => onStateChange(i)}
+ onUpdate={(value) => onUpdate(i, value)}
/>

И, наконец, мыTodoListкомпоненты, добавлениеTodoБизнес-логика после обновления данных.

TodoListкомпонентrenderЧасть примера кода внутри метода:

<Todos 
  editable
  data={this.state.todos}
+ onUpdate={(index, value) => this.handleUpdate(index, value)}
  onStateChange={(index) => this.handleStateChange(index)}
  onRemove={(index) => this.handleRemove(index)}
/>

TodoListкомпонентhandleUpdateПример кода метода:

handleUpdate(index, value) {
  let todos = [...this.state.todos];
  const target = todos[index];

  todos = [
    ...todos.slice(0, index),
    {
      text: value,
      completed: target.completed
    },
    ...todos.slice(index + 1)
  ];

  this.setState({ todos });
}

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

теперь, когдаTodoListкомпонент, начальное состояниеthis.state.todosМожно попасть снаружи. Что касается внутренней части компонента, нас не должно слишком волновать, откуда берутся данные (возможно, данные, возвращаемые родительским контейнером после прямого вызова Ajax, или данные, полученные менеджером состояний, таким как Redux, MobX и т. д. ), я думаю, что атрибут данных компонента очень важен.Дизайн можно рассматривать со следующих трех аспектов:

  • Значение по умолчанию должно быть предоставлено, когда исходные данные не передаются в
  • Как только данные обновляются внутри компонента, родительский компонент должен быть своевременно уведомлен.
  • Когда новые данные (запрошенные из внутреннего API) передаются в компонент, внутреннее состояние компонента должно снова обновляться.

На основании этих моментов мы можемTodoListСделайте еще один макияж.

Во-первых, даTodoListдобавить одинtodosАтрибут данных по умолчанию родительского компонента не повлияет на использование компонента, если не передано допустимое значение атрибута:

export default class TodoList extends Component {
  constructor(props) {
    super(props);

    this.state = {
      todos: props.todos
    };
  }
  ...
}

TodoList.defaultProps = {
  todos: []
};

Затем добавьте еще один внутренний методthis.updateи свойство обратного вызова события обновления компонентаonUpdate, родительский компонент может быть уведомлен вовремя, когда статус данных обновляется:

export default class TodoList extends Component {
  ...
  handleAdd() {
    ...
    this.update(todos);
  }

  handleUpdate(index, value) {
    ...
    this.update(todos);
  }

  handleRemove(index) {
    ...
    this.update(todos);
  }

  handleStateChange(index) {
    ...
    this.update(todos);
  }

  update(todos) {
    const { onUpdate } = this.props;

    this.setState({ todos });
    onUpdate && onUpdate(todos);
  }
}

Это конец? Нет! Нет! Нет! Потому чтоthis.state.todosНачальное состояние определяется внешнимthis.propsВходящие, если родительский компонент повторно обновляет данные, данные дочернего компонента будут не синхронизированы с родительским компонентом. Итак, как это решить?

Давайте рассмотримЖизненный цикл React, обновленные данные свойств, переданных родительским компонентом дочернему компоненту, можно найти вcomponentWillReceivePropsполучено в. Так что нам нужно повторно обновить здесьTodoListданные, о! Не забудьте оценить, согласуются ли входящие задачи с текущими данными, потому что при обновлении любых входящих реквизитов это вызоветcomponentWillReceivePropsвызывать.

componentWillReceiveProps(nextProps) {
  const nextTodos = nextProps.todos;

  if (Array.isArray(nextTodos) && !_.isEqual(this.state.todos, nextTodos)) {
    this.setState({ todos: nextTodos });
  }
}

Обратите внимание на код_.isEqual, методLodashОчень полезная функция в , я часто использую ее в этом сценарии.

конец

Из-за моего ограниченного понимания React решение в приведенном выше примере не обязательно будет самым подходящим, но вы также можете увидетьTodoListКомпоненты могут быть большими компонентами, содержащими несколько различных функциональных логик, или они могут быть разделены на независимые и умные маленькие компоненты.Я думаю, нам нужно освоить только одну степень. Конечно, как спроектировать, зависит от вашего собственного проекта, как говорится:Нет лучшего, есть только более подходящее. Тем не менее, надеюсь, что эта статья может принести вам небольшие природы.

Официальный сайт iKcamp:www.ikcamp.com

Посетите официальный веб-сайт, чтобы быстрее ознакомиться со всеми бесплатными курсами обмена: «Продюсер IKcamp | Последние онлайн-программы WeChat Mini | Обмен учебными курсами для начинающих и среднего уровня на основе инструментов разработчика последней версии 1.0». Содержит: статьи, видео, исходный код

Оригинальная новая книга iKcamp «Практика эффективной разработки мобильного веб-интерфейса» была продана на Amazon, JD.com и Dangdang.


В 2019 году оригинальная новая книга iKcamp «Практика разработки Koa и Node.js» была продана на JD.com, Tmall, Amazon и Dangdang!