Самая знакомая незнакомка rc-форма

внешний интерфейс JavaScript React.js
Самая знакомая незнакомка rc-форма

Эта статья участвовала в приказе о созыве Haowen, нажмите, чтобы просмотреть:Двойные заявки на внутреннюю и внешнюю стороны, призовой фонд в 20 000 юаней ждет вас, чтобы бросить вызов!

Это 107-я оригинальная статья без воды.Если вы хотите получить больше оригинальных и хороших статей, выполните поиск в общедоступном аккаунте и подпишитесь на нас~ Эта статья была впервые опубликована в блоге Zhengcaiyun:Самая знакомая незнакомка rc-форма

清风.png

Кто такая rc-форма?

Мы часто можем использовать сторонние библиотеки компонентов, такие как Ant Design, Element UI и Vant, для быстрого создания эффектов макета страницы и простых функций взаимодействия в проекте.

Но мы можем упустить из виду, что некоторые компоненты в этих превосходных сторонних библиотеках также могут зависеть от других превосходных библиотек! Поскольку мы очень часто используем компонент Form в Ant Design (здесь я говорю о версии React).

На самом деле, эти отличные библиотеки с открытым исходным кодом используют превосходную стороннюю библиотеку rc-form, поскольку мы часто используем такие API, как getFieldDecorator, getFieldsValue, setFieldsValue, validateFields и т. д. На самом деле все эти методы предоставляются rc-form.

Зачем использовать rc-форму?

Мы все знаем, что шаблон проектирования платформы React отличается от шаблона Vue.Автор Vue помог нам реализовать двустороннюю привязку данных, представление, управляемое данными, и изменение данных, управляемое представлением, но в React, нам нужно вручную вызвать setState, чтобы реализовать изменение представления, управляемого данными. См. код ниже.

import React, { Component } from "react";

export default class index extends Component {
  state = {
    value1: "peter",
    value2: "123",
    value3: "23",
  };

  onChange1 = ({ target: { value } }) => {
    this.setState({ value1: value });
  };

  onChange2 = ({ target: { value } }) => {
    this.setState({ value2: value });
  };
  
  onChange3 = ({ target: { value } }) => {
    this.setState({ value3: value });
  };
  
	submit = async () => {
    const { value1, value2, value3 } = this.state;
    const obj = {
      value1,
      value2,
      value3,
    };
    const res = await axios("url", obj)
  };

  render() {
    const { value1, value2, value3 } = this.state;
    return (
      <div>
        <form action="">
          <label for="">用户名: </label>
          <input type="text" value={value1} onChange={this.onChange1} />
          <br />
          <label for="">密码: </label>
          <input type="text" value={value2} onChange={this.onChange2} />
          <br />
          <label for="">年龄: </label>
          <input type="text" value={value3} onChange={this.onChange3} />
          <br />
          <button onClick={this.submit}>提交</button>
        </form>
      </div>
    );
  }
}
  • Выше приведена простая функция входа в форму! Чтобы добиться обновления данных формы в режиме реального времени, вам необходимо вручную обновить состояние состояния, когда форма находится в состоянии onChange;

  • Как видно из вышеприведенного кода, эту функцию записи тоже можно реализовать, но когда у нас много форм, нужно ли нам писать более десятка событий onChange на странице, чтобы обновить управляемое данными представление страницы ? Неуместно думать об этом таким образом;

  • В это время появилась rc-form. rc-form создает централизованное хранилище управления данными. Это хранилище отвечает за унифицированный набор логических операций, таких как проверка данных формы, сброс, настройка и получение значения. Таким образом, мы передадим повторяющуюся бесполезную работу в rc.-form для достижения высокой степени повторного использования кода!

Краткое описание основных API

Имя API иллюстрировать Типы
getFieldDecorator И формы для двусторонней связывания, Function(name)
getFieldsValue Получить значения, соответствующие набору имен полей, и вернуть их согласно соответствующей структуре. Возвращает существующее значение поля по умолчанию при вызовеgetFieldsValue(true)вернуть все значения (nameList?: NamePath[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any
getFieldValue Получить значение соответствующего имени поля (name: NamePath) => any
setFieldsValue Установить набор значений для формы (values) => void
setFields Установить набор состояний полей (fields: FieldData[]) => void
validateFields Активировать проверку формы (nameList?: NamePath[]) => Promise
isFieldValidating (name: NamePath) => boolean
getFieldProps Получает имя соответствующего поля свойства (name: NamePath) => any

import { createForm } from "../../rc-form";
// import ReactClass from './ReactClass'

const RcForm = (props) => {
  const {
    form: { getFieldDecorator, validateFields },
  } = props;

  const handleSubmit = (e) => {
    e && e.stopPropagation();
    validateFields((err, value) => {
      if (!err) {
        console.log(value);
      }
    });
  };
  return (
    <div style={{ padding: 20, background: "#fff" }} >
      <form>
        <label>姓名:</label>
        {getFieldDecorator("username", {
          rules: [{ required: true, message: "请输入用户名!" }],
          initialValue:'initialValue',
        })(<input type="text" />)}
        <br />
        <label>密码:</label>
        {getFieldDecorator("password", {
          rules: [
            { required: true, message: "请输入密码!" }, 
            { pattern: /^[a-z0-9_-]{6,18}$/, message:'只允许数字!' }
          ],
        })(<input type="password" style={{ marginTop: "15px" }} /> )}
        <br />
        <button onClick={handleSubmit} style={{ marginTop: "15px" }}>
          提交
        </button>
      </form>
    </div>
  );
};
export default createForm()(RcForm);

Примечание. После подхода createForm к компоненту (то есть в методе create() Form of Ant Design) он не будет автоматически внедрять объекты формы в компоненты, сам компонент будет иметь эти API.

  • Демо просто реализует такие функции, как оформление формы, проверка формы и сбор данных на основе rc-form. Так как же реализовать более целевые компоненты форм, подходящие для нескольких бизнес-сценариев?

  • Помимо превосходных библиотек компонентов с открытым исходным кодом, что мы должны делать, если эти отличные работы с открытым исходным кодом больше не открывают исходный код один день?

  • Чтобы этого не произошло, или если это только для нашего собственного планирования карьеры, также необходимо изучить концепции дизайна превосходных трехсторонних библиотек. Необходимо даже смотреть на стиль кода других людей. На самом деле, нам все равно нужно самим разбираться в дизайнерских идеях rc-form; только поняв суть этих прекрасных работ с открытым исходным кодом, мы сможем инкапсулировать собственную кодовую базу и отличные компоненты, такие как Form в Ant Design, даже если мы этого не делаем. нужны библиотеки с открытым исходным кодом.

Начните с создания формы

Все мы знаем, что обычно используем методы createForm или Form.create() для упаковки наших собственных компонентов всякий раз, когда мы используем формы для написания бизнес-компонентов, поэтому мы начнем наш рассказ отсюда.

import createBaseForm from './createBaseForm';

function createForm(options) {
  return createBaseForm(options, [mixin]);
}

export default createForm;

Видно, что createForm — это всего лишь слой инкапсуляции, а настоящей вызывающей функцией является createBaseForm, поэтому давайте сосредоточимся на внутренней реализации функции createBaseForm.

createBaseForm

Как видно из приведенного выше рисунка, эта функция использует функцию замыкания для возврата новой функции. Параметр этой функции на самом деле является объектом вашего бизнес-компонента. После внутренней обработки createBaseForm вам возвращается компонент, внедренный с объект формы. То есть мы часто говорим, что эта createBaseForm является компонентом высокого порядка.

Тогда становится ясно, что Ant DesignForm.create()Путь этоrc-formсерединаcreateBaseFormАльтернатива методу! проходить черезcreateBaseFormОбернутый компонент будет внедрять объект формы иformЭкземпляры getFieldDecorator и fieldsStore, представленные в свойствах, являются ключом к автоматическому сбору данных.

Анализ внутренней реализации

Начнем с начальной логики отрисовки формы.Компоненты формы, используемые в наших бизнес-сценариях, будут обернуты с помощью getFieldDecorator. Конечно, я говорю о версиях до Ant Design 4.0, так что давайте начнем отсюда.

Позвольте мне сначала объяснить, что в этой статье я просто анализирую простой процесс двусторонней привязки всех данных формы, потому что это ядро ​​rc-формы, а конкретные детали ограниченной энергии оставлю для изучения позже. . Итак, давайте посмотримgetFieldDecoratorЧто делает метод?

getFieldDecorator(name, fieldOption) {
  const props = this.getFieldProps(name, fieldOption);
  return fieldElem => {
    // We should put field in record if it is rendered
    this.renderFields[name] = true;

    const fieldMeta = this.fieldsStore.getFieldMeta(name);
    const originalProps = fieldElem.props;
    fieldMeta.originalProps = originalProps;
    fieldMeta.ref = fieldElem.ref;
    const decoratedFieldElem = React.cloneElement(fieldElem, {
      ...props,
      ...this.fieldsStore.getFieldValuePropValue(fieldMeta),
    });
    return supportRef(fieldElem) ? (
      decoratedFieldElem
    ) : (
      <FieldElemWrapper name={name} form={this}>
      {decoratedFieldElem}
  </FieldElemWrapper>
  );
};
},

Я удалил здесь ненужный код, потому что он выглядит чище. Сначала вызовите метод getFieldProps входящего компонента формы, чтобы создать свойства, а затем верните функцию.Этот параметр функции является компонентом формы, который мы передали с помощью getFieldDecorator, и вызовите getFieldMeta в fieldsStore, чтобы получить данные конфигурации компонента формы. , который совместим с оригиналом. Свойства конфигурации компонентов и обработка компонентов, не поддерживающих ref, наконец, возвращают клонированный компонент, который после обработки монтирует некоторые объекты конфигурации!

fieldsStore

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

class FieldsStore {
  constructor(fields) {
    this.fields = internalFlattenFields(fields);
    this.fieldsMeta = {};
  }
}

fieldMeta можно рассматривать как описание элемента формы с входящим именем в качестве ключа индекса и поддерживает вложение и хранение данных формы, то есть информация о конфигурации не включает значения, в основном в том числе:

  • имя поля имени
  • originalProps исходные реквизиты компонента, оформленные функцией getFieldDecorator().
  • правила проверки правил
  • onChange
  • Проверьте правила проверки и спусковые события
  • valuePropName Свойство значения дочернего узла, например, флажок должен быть установлен наchecked
  • Как getValueFromEvent получить значение компонента из события
  • Если для hidden установлено значение true, это поле будет игнорироваться при проверке или сборе данных.

поля в основном используются для записи атрибутов каждой формы в реальном времени, в том числе:

  • Изменились ли грязные данные, но не проверены

  • копия проверки ошибок

  • имя поля имя

  • коснулись ли данные были обновлены

  • Поле значение значения

  • проверка статуса проверки

Тогда давайте посмотрим, как метод getFieldProps реализует построение реквизита?

getFieldProps(name, usersFieldOption = {}) {
  // 重新组装 props
  const fieldOption = {
    name,
    trigger: DEFAULT_TRIGGER,
    valuePropName: 'value',
    validate: [],
    ...usersFieldOption,
  };
  const {
    rules,
    trigger,
    validateTrigger = trigger,
    validate,
  } = fieldOption;
  const fieldMeta = this.fieldsStore.getFieldMeta(name);
  // 初始值处理
  if ('initialValue' in fieldOption) {
    fieldMeta.initialValue = fieldOption.initialValue;
  }
	// 组装 inputProps
  const inputProps = {
    ...this.fieldsStore.getFieldValuePropValue(fieldOption),
    ref: this.getCacheBind(name, `${name}__ref`, this.saveRef),
  };
  if (fieldNameProp) {
    inputProps[fieldNameProp] = formName ? `${formName}_${name}` : name;
  }
	
  // 收集验证规则
  const validateRules = normalizeValidateRules(validate, rules, validateTrigger);
  const validateTriggers = getValidateTriggers(validateRules);
  validateTriggers.forEach((action) => {
    if (inputProps[action]) return;
    inputProps[action] = this.getCacheBind(name, action, this.onCollectValidate);
  });

  // 不走效验的组件使用 onCollect 收集组件的值
  if (trigger && validateTriggers.indexOf(trigger) === -1) {
    inputProps[trigger] = this.getCacheBind(name, trigger, this.onCollect);
  }

  return inputProps;
},

Удалил немного подробного кода, давайте взглянем на getFieldProps Сначала обрабатывается значение по умолчанию, если пользователь его не установилtriggerа такжеvaluePropNameзатем используйте значение по умолчанию, затем вызовитеfieldsStoreсерединаgetFieldMetaметод,fieldsStoreformfieldsStore.getFieldMetaВы работали?

getFieldMeta(name) {
  this.fieldsMeta[name] = this.fieldsMeta[name] || {};
  return this.fieldsMeta[name];
}

Функция этой функции — получить fieldMeta центра обработки данных по атрибуту name, переданному компонентом, если нет, то по умолчанию будет пустой объект, то есть будет возвращено начальное значение при первом рендеринге. Важная вещь — ссылка на сборку inputProps, первый шаг — вызватьgetFieldValuePropValueМетод получает текущие реквизиты, затем добавляет атрибут ref, за которым следует набор правил проверки.

const validateRules = normalizeValidateRules(validate, rules, validateTrigger);
const validateTriggers = getValidateTriggers(validateRules);
validateTriggers.forEach((action) => {
    if (inputProps[action]) return;
    inputProps[action] = this.getCacheBind(name, action, this.onCollectValidate);
});

if (trigger && validateTriggers.indexOf(trigger) === -1) {
    inputProps[trigger] = this.getCacheBind(name, trigger, this.onCollect);
}

validateRules То есть все правила проверки компонентов формы,validateTriggersТо есть название события, срабатывающего по всем правилам валидации, то давайте посмотримnomalizeValidateRulesтак же какgetValidateTriggers Как методы собирают правила проверки.

function normalizeValidateRules(validate, rules, validateTrigger) {
  const validateRules = validate.map((item) => {
    const newItem = {
      ...item,
      trigger: item.trigger || [],
    };
    if (typeof newItem.trigger === 'string') {
      newItem.trigger = [newItem.trigger];
    }
    return newItem;
  });
  if (rules) {
    validateRules.push({
      trigger: validateTrigger
      ? [].concat(validateTrigger)
      : [],
      rules,
    });
  }
  return validateRules;
}

function getValidateTriggers(validateRules) {
  return validateRules
    .filter(item => !!item.rules && item.rules.length)
    .map(item => item.trigger)
    .reduce((pre, curr) => pre.concat(curr), []);
}

Так и будетvalidate,rulesОбъедините, верните массив, внутренние элементы которого являются объектами правил, и каждый элемент имеет значение, допускающее значение NULL.triggerмассив и будетvalidateTriggerтак какruleизtriggersТолкатьvalidateRulesМы оглядываем назадvalidateTrigger.

const fieldOption = {
     name,
     trigger: DEFAULT_TRIGGER,
     valuePropName: 'value',
     validate: [],
     ...usersFieldOption,
 };

const {
    rules,
    trigger,
    validateTrigger = trigger,
    validate,
} = fieldOption;

Здесь видно, что если пользователь настраивает триггерный метод аутентификации, настроенный используется по умолчанию.triggerЕсли вы не установитеtriggeronChange.

getValidateTriggersОн заключается в том, чтобы собрать все триггерные события в массив, а затем передать цикл forEach всемvalidateTriggers События в привязаны к одной и той же функции-обработчику getCacheBind.

 validateTriggers.forEach((action) => {
 	if (inputProps[action]) return;
 	inputProps[action] = this.getCacheBind(
    name, 
    action, 
    this.onCollectValidate
  );
 });

Давайте посмотрим, что делает функция getCacheBind, запускающая действие события привязки правила проверки.

getCacheBind(name, action, fn) {
  if (!this.cachedBind[name]) {
    this.cachedBind[name] = {};
  }
  const cache = this.cachedBind[name];
  if (
    !cache[action] ||
    cache[action].oriFn !== fn 
  	) {
    cache[action] = {
      fn: fn.bind(this, name, action),
      oriFn: fn,
    };
  }
  return cache[action].fn;
},

Пока игнорируйте метод cachedBind, здесь вы можете видеть, что метод getCacheBind в основном выполняет логическую обработку изменения указателя this на входящий fn, а реальная функция обработкиonCollectValidate, тогда давайте посмотримonCollectValidate Что вы наделали?

onCollectValidate(name_, action, ...args) {
  const { field, fieldMeta } = this.onCollectCommon(name_, action, args);
  const newField = {
    ...field,
    dirty: true,
  };
  this.fieldsStore.setFieldsAsDirty();
  
  this.validateFieldsInternal([newField], {
    action,
    options: {firstFields: !!fieldMeta.validateFirst,},
  });
},

когдаonCollectValidate Вызывается, то есть при срабатывании функции проверки данных сначала вызывается метод onCollectCommon, так что же делает эта функция?

onCollectCommon(name, action, args) {
  const fieldMeta = this.fieldsStore.getFieldMeta(name);
  if (fieldMeta[action]) {
    fieldMeta[action](...args);
  } else if (fieldMeta.originalProps && fieldMeta.originalProps[action]) {
    fieldMeta.originalProps[action](...args);
  }
  const value = fieldMeta.getValueFromEvent ?
        fieldMeta.getValueFromEvent(...args) :
  getValueFromEvent(...args);
  if (onValuesChange && value !== this.fieldsStore.getFieldValue(name)) {
    const valuesAll = this.fieldsStore.getAllValues();
    const valuesAllSet = {};
    valuesAll[name] = value;
    Object.keys(valuesAll).forEach(key => set(valuesAllSet, key, valuesAll[key]));
    onValuesChange({
      [formPropName]: this.getForm(),
      ...this.props
    }, set({}, name, value), valuesAllSet);
  }
  const field = this.fieldsStore.getField(name);
  return ({ name, field: { ...field, value, touched: true }, fieldMeta });
},

onCollectCommon В основном, чтобы получить последнее значение упакованного компонента, затем обернуть его в объект и вернуть, а после возврата собрать в новое имя.newField Объект.

а такжеfieldsStore.setFieldsAsDirtyОн заключается в том, чтобы отметить статус проверки компонента упаковки, пропустить его на данный момент, а затем выполнитьvalidateFieldsInternal, давайте взглянем на функцию validateFieldsInternal.

validateFieldsInternal( 
  fields,
  { fieldNames, action, options = {} },
  callback,
) {
  const allRules = {};
  const allValues = {};
  const allFields = {};
  const alreadyErrors = {};
  fields.forEach(field => {
    const name = field.name;
    if (options.force !== true && field.dirty === false) {
      if (field.errors) {
        set(alreadyErrors, name, { errors: field.errors });
      }
      return;
    }
    const fieldMeta = this.fieldsStore.getFieldMeta(name);
    const newField = {
      ...field,
    };
    newField.errors = undefined;
    newField.validating = true;
    newField.dirty = true;
    allRules[name] = this.getRules(fieldMeta, action);
    allValues[name] = newField.value;
    allFields[name] = newField;
  });
  this.setFields(allFields);
  // in case normalize
  Object.keys(allValues).forEach(f => {
    allValues[f] = this.fieldsStore.getFieldValue(f);
  });
  if (callback && isEmptyObject(allFields)) {
    callback(
      isEmptyObject(alreadyErrors) ? null : alreadyErrors,
      this.fieldsStore.getFieldsValue(fieldNames),
    );
    return;
  }
  // console.log(allRules);
  const validator = new AsyncValidator(allRules);
  if (validateMessages) {
    // console.log(validateMessages);
    validator.messages(validateMessages);
  }
  validator.validate(allValues, options, errors => {
    const errorsGroup = {
      ...alreadyErrors,
    };
    // ...
    const expired = [];
    const nowAllFields = {};
    Object.keys(allRules).forEach(name => {
      const fieldErrors = get(errorsGroup, name);
      const nowField = this.fieldsStore.getField(name);
      // avoid concurrency problems
      if (!eq(nowField.value, allValues[name])) {
        expired.push({
          name,
        });
      } else {
        nowField.errors = fieldErrors && fieldErrors.errors;
        nowField.value = allValues[name];
        nowField.validating = false;
        nowField.dirty = false;
        nowAllFields[name] = nowField;
      }
    });
    this.setFields(nowAllFields);
    // ...
  }

потому чтоvalidateFieldsInternalОсновная логика вызоваAsyncValidatorДля асинхронной проверки и обработки особых сценариев временно пропускаем часть сбора данных, видим, что вызов осуществляется в концеthis.setFields(allFields);И передайте новое значение, затем посмотрите наsetFieldsметод.

setFields(maybeNestedFields, callback) {
  const fields = this.fieldsStore.flattenRegisteredFields(maybeNestedFields);
  this.fieldsStore.setFields(fields);
  if (onFieldsChange) {
    const changedFields = Object.keys(fields)
    .reduce((acc, name) => set(acc, name, this.fieldsStore.getField(name)), {});
    onFieldsChange({
      [formPropName]: this.getForm(),
      ...this.props
    }, changedFields, this.fieldsStore.getNestedAllFields());
  }
  this.forceUpdate(callback);
},

Мы видим, что,setFieldsСначала входящее значение проверяется аналогично инициализации, а затем вызывается метод setFields в экземпляре fieldsStore для сохранения значения вfieldsStore, игнорируйте покаonFieldsChangeЗатем позвонитеforceUpdate

Двусторонняя форма привязки данных

forceUpdate

Суммировать:

  • Если мы не настраиваем FaideAreureules и ValidatEtriggers и другие правила, затем используйте метод oncoLlect, чтобы собрать последние данные и обновить его в fieldStore. Вместо того, чтобы подтвердить форму индивидуально, вызовите это. OFFOUPDATE () в методе SetFields для обновления View UI!

общая идея дизайна

fremework

Суммировать:

  • Короче говоря, rc-form имеет собственное управление состоянием внутри, fieldsStore записывает информацию обо всех элементах формы и связывает форму в двух направлениях через getFieldDecorator;
  • Реальная разница заключается в том, следует ли использовать проверку правил формы или нет, onCollect, если нет, в противном случае используется onCollectValidate, но необходимо использовать onCollectCommon;
  • Метод onCollectCommon показывает сведения о значении onCollect.После обновления компонента forceUpdate запускает метод рендеринга, а затем возвращается к началу getFieldDecorator, чтобы получить значение в fieldStore, и возвращает измененный компонент.

Подумайте, если я изменю значение поля ввода, это вызовет повторную визуализацию формы. Так что это также приводит к проблеме производительности рендеринга!Тогда должен быть метод оптимизации.Если вам интересно, вы можете взглянуть на rc-field-form.

Статья представляет собой лишь общий анализ реализации идей, если у вас есть разные мнения, пожалуйста, свяжитесь со мной для обмена!

Рекомендуемое чтение

Возможности Vite и частичный анализ исходного кода

Как я использую git на работе

Serverless Custom (Container) Runtime

работы с открытым исходным кодом

  • Zhengcaiyun интерфейсный таблоид

адрес с открытым исходным кодомwww.zoo.team/openweekly/(На главной странице официального сайта таблоида есть группа обмена WeChat)

Карьера

ZooTeam, молодая, увлеченная и творческая команда, связанная с отделом исследований и разработок продукции Zhengcaiyun, базируется в живописном Ханчжоу. В настоящее время в команде более 40 фронтенд-партнеров, средний возраст которых составляет 27 лет, и почти 30% из них — инженеры полного стека, настоящая молодежная штурмовая группа. В состав членов входят «ветераны» солдат из Ali и NetEase, а также первокурсники из Чжэцзянского университета, Университета науки и технологий Китая, Университета Хандянь и других школ. В дополнение к ежедневным деловым связям, команда также проводит технические исследования и фактические боевые действия в области системы материалов, инженерной платформы, строительной платформы, производительности, облачных приложений, анализа и визуализации данных, а также продвигает и внедряет ряд внутренних технологий. Откройте для себя новые горизонты передовых технологических систем.

Если вы хотите измениться, вас забрасывают вещами, и вы надеетесь начать их бросать; если вы хотите измениться, вам сказали, что вам нужно больше идей, но вы не можете сломать игру; если вы хотите изменить , у вас есть возможность добиться этого результата, но вы не нужны; если вы хотите изменить то, чего хотите достичь, вам нужна команда для поддержки, но вам некуда вести людей; если вы хотите изменить установившийся ритм, это будет "5 лет рабочего времени и 3 года стажа работы"; если вы хотите изменить исходный Понимание хорошее, но всегда есть размытие того слоя оконной бумаги.. , Если вы верите в силу веры, верьте, что обычные люди могут достичь необыкновенных вещей, и верьте, что они могут встретить лучшего себя. Если вы хотите участвовать в процессе становления бизнеса и лично способствовать росту фронтенд-команды с глубоким пониманием бизнеса, надежной технической системой, технологиями, создающими ценность, и побочным влиянием, я думаю, что мы должны говорить. В любое время, ожидая, пока вы что-нибудь напишете, отправьте это наZooTeam@cai-inc.com