Машинописный текст с практикой React

Архитектура внешний интерфейс TypeScript React.js

Статья впервые опубликована:Машинописный текст с практикой React


Я использую ts для написания кода React почти три месяца, с самого начала я чувствовал, что это очень херня, а теперь я чувствую, что есть некоторые практики и мысли, которые ts не может сделать. Если вы будете писать React шаг за шагом, вы не получите удовольствия от использования TS. Я принес боль, о которой я расскажу в конце этой статьи.

Изменения в мышлении с помощью TS

Сначала я думал, что ts — это ерунда, я думал, что React — этоPropTypeа такжеPropDefaultОн может делать почти то же, что и статическая проверка типов ts, и даже больше, чем ts. Например, для установки дефолтных значений между компонентами ts не очень подходит для поддержки.

Позже, в связи с потребностью, немного передумал, мысль на тот момент была: "Не говори, этот тс еще пригодится". В этом сценарии возможны две ситуации:

  1. Имя параметра, передаваемое родительским компонентом дочернему компоненту, должно измениться в соответствии с предыдущимcommamd(ctrl) + fСпособ искать и модифицировать глобально, но это все равно очень недружественно, если объем большой (то, что я встречал, это случай большого объема), если равномерно заменить, например, эта переменная называется пользовательской, там высокий вероятность того, что он будет содержать другие переменные, поэтому единая замена будет неудобной. Но статическая проверка типов ts решает эту проблему за вас: для каждого значения, не переданного родительским компонентом, будет выдаваться сообщение об ошибке. и тсОшибка во время компиляции, а не во время выполнения.
  2. Однако, если имя переданного параметра не изменится, а значение параметра изменится, статический тип ts также проверит это за вас, а затем разработчик внесет изменения. Сказав, что они относительно абстрактны, код предыдущего примера относительно ясен:
// 父组件
render(): ReactNode {
    const { user, loading, namespaceList, yarnList, workspace } = this.state;
    return (
      <UserDetail
        user={user}
        loading={loading}
        namespaceList={namespaceList}
        yarnList={yarnList}
        workspace={workspace}
        onLoadWorkspace={(params: IParams) => this.onLoadWorkspace(params)}
        onUpdateUser={(field: string, data: IUserBaseInfo) => this.handleUpdateUser(field, data)}
        onToCreateAk={(userId: number) => this.handleToCreateAk(userId)}
        onDelete={(ids: number[]) => this.handleDelete(ids)}
        onUpdateResource={(userId: number, data: IResources) => this.onUpdateResource(userId, data)}
        onAkDelete={(ak: IPermanentKey) => this.handleDeleteAk(ak)}
        onChangeAkStatus={(ak: IPermanentKey, status: string) => this.onChangeAkStatus(ak, status)}
      />
    );
  }

Просто обратите внимание на первый параметр, это собственно бизнес-сценарий, далее идут подкомпоненты:

export interface IProps {
 user: IUser | null;
 loading: boolean;
 namespaceList: { namespace: string }[];
 yarnList: IYarn[];
 workspace: {
   list: IWorkspace[] | null,
   total: number,
 } | null;
 onLoadWorkspace: (params: IParams) => void;
 onUpdateUser: (field: string, data: IUserBaseInfo) => void;
 onToCreateAk: (userId: number) => void;
 onDelete: (ids: number[]) => void;
 onUpdateResource: (userId: number, data: IResources) => void;
 onAkDelete: (ak: IPermanentKey) => void;
 onChangeAkStatus: (ak: IPermanentKey, status: string) => void;
}

class UserDetail extends Form<IProps, {}> {

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

Когда я стиснул зубы и готовился модифицировать тысячи строк React-кода моих коллег, я сначала долго колебался, опасаясь, что не успею закончить до релиза. не нужно так заботиться. Можно приблизительно определить значения и обратные вызовы, передаваемые родительским компонентом дочернему компоненту. Может быть, будет немного грубо сказать, что если написать компонент самостоятельно, это то же самое, ха-ха. Позже мы особо упомянем, как использовать ts для рефакторинга. В это время менталитет ТС таков: «эта вещь действительно мощная».

После нескольких раз рефакторинга самого себя и рефакторинга кода других людей, мое текущее отношение к ts таково: «Возможно, я буду неотделим от этого материала в своей будущей карьере фронтенда».

Архитектура проекта

потому что вы можете найти его в Интернетеts+reactПроектов пока относительно немного, а реальных практик сравнительно мало, и все они настроены с нуля. До сих пор нет конкретного практического решения, как улучшить структуру каталогов файла. Конечно, это решение еще нужно проанализировать в соответствии с конкретным бизнесом. в предыдущей статьеПишите код React без избыточностиОпишите бизнес-сценарий, с которым я сейчас сталкиваюсь.

наконец решил выложить всеinterfaceположить все этообщедоступный каталог схемЗатем сделайте конкретные ссылки в конкретных бизнесах. Конкретная общая структура каталогов выглядит следующим образом (вся информация об интерфейсе хранится в каталоге schemas):

common
├── component
│   ├── delete-workspace-modal
│   │   ├── delete-workspace-modal.less
│   │   ├── delete-workspace-modal.less.d.ts
│   │   └── index.tsx
│   └── step-complete
│       ├── index.tsx
│       ├── step-complete.less
│       └── step-complete.less.d.ts
├── css
│   └── global.less
├── hoc
│   ├── workspace-detail.tsx
│   └── workspace-list.tsx
├── schemas
│   ├── dialog.ts
│   ├── k8s.ts
│   ├── ldap.ts
│   ├── message.ts
│   ├── params.ts
│   ├── password.ts
│   ├── section.ts
│   ├── table.ts
│   ├── user.ts
│   ├── workspace.ts
│   └── yarn.ts
└── util
    ├── field-value.ts
    ├── format-datetime.ts
    ├── genURL.ts
    ├── getNamespaceList.ts
    ├── getYarnList.ts
    └── validation.ts

Файлы в каталоге schemas аналогичны общим статическим типам, которые связаны с бизнесом, но не привязаны строго к определенному модулю, потому что неизбежно будет некоторое пересечение между каждым модулем. Ниже приведен статический тип конкретного модуля:

export interface IYarnResource {
  id: number;
  namespace: string;
  user: string;
  queue: string;
}

export interface IYarnStatus {
  name: string;
  error: string;
  maxCapacity: number;
  state: string;
  used: number;
  capacity: number;
}

export interface IYarnEntity extends IYarnResource {
  status: IYarnStatus;
  keytab: string;
}

Статические типы, которые сильно связаны с модулями, такими какpropsа такжеstateСтатические типы , будут помещены в абсолютный бизнес-файл, например следующий код (упрощенный):

import React, { PureComponent, ReactNode, Fragment } from 'react';
import { IComplex } from 'common/schemas/password';
export interface IProps {
  onClose(): void;
  onOK(data: IComplex): void;
  complex: IComplex | null;
}
export interface IState extends IComplex {
}
class PasswordComplex extends PureComponent<IProps, IState> {
    state: IState = {
    leastLength: 6,
    needCapitalLetter: false,
    needLowercaseLetter: false,
    needNumber: false,
    needSpecialCharacter: false,
  };
}

Все статические бизнес-типы, как правило, не предназначены для повторного использования и обычно состоят из общих статических типов и некоторых специальных статических типов.

Инициализацию состояния не нужно помещать вconstructorвнутри, но вы должны указать тип состояния. Конкретные причины см. в:Typescript in React: State will not be placed in the constructor will cause an error

Конкретные методы статической типизации

если бы мы установили@types/react, в каталоге реакцииindex.d.tsТам будут все определения статического типа для реакции.

специфическая компонентная архитектура

Теперь, например, напишите модуль управления пользователями, который содержитПросмотр сведений о пользователе,Посмотреть список пользователей,новый пользовательи другие функции. Это соответствует трем маршрутам/users/:id,/users,/users/create. Это соответствует трем компонентам с отслеживанием состояния:user-detail-wrapper, user-list-wrapper,user-form-wrappper. Компоненты с отслеживанием состояния только запрашивают или получают данные и так далее. Дисплей черезcomponentКомпонент без гражданства ниже. Взгляните на следующую структуру каталогов:

user
├── component
│   ├── password-complex
│   │   ├── index.tsx
│   │   ├── password-complex.less
│   │   └── password-complex.less.d.ts
│   ├── user-detail
│   │   ├── index.tsx
│   │   ├── user-detail.less
│   │   └── user-detail.less.d.ts
│   ├── user-detail-ak
│   │   ├── index.tsx
│   │   ├── user-detail-ak.less
│   │   └── user-detail-ak.less.d.ts
│   ├── user-detail-base-info
│   │   ├── index.tsx
│   │   ├── user-detail-base-info.less
│   │   └── user-detail-base-info.less.d.ts
│   ├── user-detail-resource
│   │   ├── index.tsx
│   │   ├── user-detail-resource.less
│   │   └── user-detail-resource.less.d.ts
│   ├── user-detail-workspace
│   │   └── index.tsx
│   ├── user-form-dialog
│   │   ├── index.tsx
│   │   ├── user-form-dialog.less
│   │   └── user-form-dialog.less.d.ts
│   └── user-list
│       ├── index.tsx
│       ├── user-list.less
│       └── user-list.less.d.ts
├── user-form-wrapper
│   └── index.tsx
├── user-detail-wrapper
│   └── index.tsx
└── user-list-wrapper
    └── index.tsx

компоненты с состоянием

установить состояние только для чтения

Я видел много практики в Интернете, чтобы предотвратитьstateнельзя вмешиваться, это будетstateСделать его доступным для чтения можно следующим образом. Хотя это и хорошо, но он не появится в моем проекте. Такая ошибка совершается только новыми людьми, которые имеют контакт с React или уже писали Vue. проект, есть в общей сложности два человека, не появляются в такого рода проблемы.

const defaultState = {
  name: string;
}

type IState = Readonly<typeof defaultState>

class User extends Component<{}, IState> {
  readonly state: IState = defaultState;
}

Но вышеописанный метод подходит только для типовосновные типы машинописного текста, но если у вас есть сложный тип, определенный вами, например следующий:


interface IUser {
  name: string;
  id: number:
 age: number;
  ...
}

interface IState {
  list: IUser[];
  total: number;
}
// default state

const userList: IUser = []
const defaultState = {
  list: userList,
  total: 0,
}

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

компоненты без сохранения состояния

Компоненты без состояния также известны как презентационные компоненты. Если презентационный компонент не имеет внутреннего состояния, его можно записать как чисто функциональный компонент. Если вы пишете функциональный компонент, в@types/reactопределяет тип вtype SFC<P = {}> = StatelessComponent<P>;. Когда мы пишем функциональные компоненты, мы можем указать, что наши компонентыSFCилиStatelessComponent. Это уже предопределеноchildrenПодождите, чтобы нам не нужно было каждый раз указывать тип дочерних элементов. Вот пример компонента без состояния:

import React, { ReactNode, SFC } from 'react';
import style from './step-complete.less';

export interface IProps  {
  title: string | ReactNode;
  description: string | ReactNode;
}
const StepComplete:SFC<IProps> = ({ title, description, children }) => {
  return (
    <div className={style.complete}>
      <div className={style.completeTitle}>
        {title}
      </div>
      <div className={style.completeSubTitle}>
        {description}
      </div>
      <div>
        {children}
      </div>
    </div>
  );
};
export default StepComplete;

Общие компоненты

Сначала посмотрите на компонент, этот компонент должен отображать список.

import React, { Fragment, PureComponent } from 'react';

export interface IProps<T> {
  total: number;
  list: IYarn[];
  title: string;
  cols: IColumn[];
}

class YarnList  extends PureComponent<IProps> {

}

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

export interface IProps<T> {
  total: number;
  list: T[];
  title: string;
  cols: IColumn[];
}

class ResourceList<T> extends PureComponent<IProps<T>> {
  // 我们现在业务的场景会把这个list传递给table,table不同的字段通过外部的父组件传递进来。
tableProps(): ITable<T> {
    const columns: IColumn[] = [
      ...this.props.cols,
      { title: '操作', key: 'operation', render: (record: T) => this.renderOperation(record) },
    ];
    return {
      columns,
      data: this.props.list,
      selectable: false,
      enabaleDefaultOperationCol: false,
      searchEmptyText: '没有搜索到符合条件的资源',
      emptyText: '尚未添加资源',
    };
  }
}

установить значение по умолчанию

Если вы используете машинописный текст версии 3.x, вам не нужно беспокоиться об этой проблеме, просто используйте его непосредственно в jsx.defaultPropsВот и все.

Если вы используете версию 2.x, вам необходимо определить необязательное значение, подобное следующему:

interface IProps {
  name?: string;
}

Если мы установим в классе defaultProps, ts об этом не узнает. Нужно еще сделать непустое суждение в коде. Это отличный способ написать компонент более высокого порядка.источник компонентов более высокого порядка

export const withDefaultProps = <
  P extends object,
  DP extends Partial<P> = Partial<P>
>(
  defaultProps: DP,
  Cmp: ComponentType<P>,
) => {
  // 提取出必须的属性
  type RequiredProps = Omit<P, keyof DP>;
  // 重新创建我们的属性定义,通过一个相交类型,将所有的原始属性标记成可选的,必选的属性标记成可选的
  type Props = Partial<DP> & Required<RequiredProps>;

  Cmp.defaultProps = defaultProps;

  // 返回重新的定义的属性类型组件,通过将原始组件的类型检查关闭,然后再设置正确的属性类型
  return (Cmp as ComponentType<any>) as ComponentType<Props>;
};

Что плохого в машинописном тексте

Определить тип немного трудоемко, а иногда требуется большая часть усилий, чтобы обнаружить, что это проблема всего типа ts. Потом. . . Должен исчезнуть.

Спецификация фронтенд-разработки

Вот основное введение в спецификации персонального развития при написании компонентов:

  • Содержание поля должно быть объяснено до конца, насколько это возможно.
    • Пример: Компонент хочет передать параметр объекта дочернему элементу (дочернему элементу...), но теперь возможный компонент использует только поле имени. В целях расширения не следует просто передавать имя этому дочернему элементу (дочернему элементу...) свойство, чтобы передать весь объект в прошлое.
    • Пример: компонент без сохранения состояния может изменить имя пользователя. При нажатии кнопки ОК для его изменения не просто передать измененное имя обратно, а передать все обратно.
  • Компоненты с отслеживанием состояния обрабатывают только логику ответов и запросов, а не какую-либо информацию о представлении. То есть в компоненте с отслеживанием состоянияrenderФункция только передает информацию дочернему компоненту
  • Компоненты без сохранения состояния могут сохранять некоторую информацию о состоянии, например отображение и скрытие всплывающего окна.
  • Компонент не может превышать 300 строк кода
  • Отступ в две строки (разные компиляторы используют .editorconfig)
  • универсальныйinterfaceпомещатьcommonпоследующийschemasпод
  • неуниверсальныйinterfaceНапримерIPropsилиIStateдля размещения внутри компонента
  • Абстрактные вещи, которые можно использовать более чем в двух местах

Ссылаться на