Pull to refresh
47
0
Дмитрий Казаков @DmitryKazakov8

Front-end архитектор

Send message

Непрофессиональный цикл статей, на мой взгляд. Даже в простейших примерах явные ошибки. То setState как будто синхронный, то текстовые пропы от руки каждый раз пишутся вместо констант, то вот label здесь без htmlFor. Если кто-то все же читает, лучше используйте такой паттерн:


<Input.Text
  label="Name"
  storePath="forms.registration.email"
/>

// и внутри компонента

const id = generateId(props.storePath);

return (
  <div>
    <label htmlFor={id}>{props.label}</label>
    <input type="text" id={id} value={getValueFromStore(props.storePath)}/>
  </div>
)

Акцент на автоматической генерации id, удобстве получения данных из стора, и фокусировании инпута при клике на label.


В примере storePath строкой задано, что при рефакторинге может вылезти в баг, поэтому тоже лучше генерировать storePath={pathToString(forms.registration.email)}

Дипломатические методы — обсуждение, убеждение, презентации для руководства разного уровня — могут принести достаточно ресурсов, чтобы сделать большую часть проекта качественно. Я в нескольких компаниях на цифрах показывал, сколько убытков в деньгах и времени приносит некачественный быстрокод и засилье «дешевых» разработчиков в проекте — и если для руководства важен бизнес и результат, а не распил, то непременно пойдут навстречу. Не с первого раза, так с десятого.

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


Button.appearance = {
  default: 'default',
  primary: 'primary',
  secondary: 'secondary',
}

<Button appearance={Button.appearance.primary}>Click me</Button>
«Извращение», «все неправильно» и особенно «все уже поняли что всем нужна типизация», как модно было говорить в докладах на недавних конференциях, для меня не аргументы использовать эту технологию.

Если бы я знал о явных преимуществах и они стоили всех мучений и долгого обучения — как вы правильно заметили, за полгода команда довольно сильных специалистов не научилась «правильно работать со строгой типизацией», хотя в основном проблемы зависели не от команды, то можно было бы дальше возиться с этим и обильно осваивать бюджет компании. Но нет. Предпочитаю делать продуманные качественные продукты и приносить пользу, а не возиться с почти бесполезными инструментами, не двигающими к этой цели.

Я не говорю за всех разработчиков и за все проекты — как я писал выше, в отдельных библиотеках использование типизации скорее оправдано, а в сложных многосоставных системах вроде SPA явно негативный эффект — замедление разработки без видимых профитов. IMHO
Я описал только малую часть проблем — каждый день несколько часов работы команды уходило на типизацию и борьбу с негативными эффектами. Разумеется, все можно победить — вопрос зачем? Пользу от типизации в SPA я до сих пор не увидел.

Ошибки в типах в рантайме только увеличились. Рефакторинг стал сложнее — в ES6 проекте 90% рефакторинга выполняется findAndReplace с помощью регулярок, с типизацией работа удваивается. Переменные как были с опечатками или не отражающие назначения, так и остались. В опенсорс-зависимостях очень часто выходят версии с некорректными типами, ломающими сборку, и приходится форкать и вручную исправлять. Перечислять минусы могу еще долго.

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

Доброго дня.


Undo/Redo на большую глубину, сериализация состояния и истории того, как приложение в это состояние попало



тривиально реализуется через autorun. Можно делать глобальные слепки -


autorun(() => snapshots.push(JSON.stringify(store)))

очищая от функций и ненужных данных, разумеется, либо пушить только изменения конкретных данных


autorun(reaction => reaction.observing.forEach(
  ({ name, value }) => changesHistory.push({ name, value, prevValue })
)

Проигрывание этой истории и логирование последних действий при возникновении глобальной ошибки, полагаю, тоже сделать несложно — мне не пригождалось, так как стараюсь решать проблему поиска причины залогированного бага с помощью именованного stackTrace, типизированного error.name и человекопонятного error.message.


Поэтому я бы не рассматривал встроенный в Redux механизм истории как серьезный аргумент в его пользу — если он действительно нужен, реализовать его довольно просто.


По теме гонок и каскадных обновлений Redux, как вы правильно заметили, своим диспетчером решает проблему «частично», и мне, к сожалению, приходилось работать в приложениях, где «все остальные части» не решались, и они очень страдали от нагромождения бойлерплейтов и дополнительных библиотек и мидлвар, в которых крайне сложно было создать стабильный флоу. Это не камень в огород Redux, я скорее о том, что проектирование архитектуры никто не отменяет.

src/utils/withState.js
export function withState(target, fnName, fnDescriptor) {
  const original = fnDescriptor.value;

  fnDescriptor.value = function fnWithState(...args) {
    if (this.executions[fnName]) {
      return Promise.resolve();
    }

    return Promise.resolve()
      .then(() => {
        this.executions[fnName] = true;
      })
      .then(() => original.apply(this, args))
      .then(data => {
        this.executions[fnName] = false;
        return data;
      })
      .catch(error => {
        this.executions[fnName] = false;
        throw error;
      });
  };

  return fnDescriptor;
}

src/stores/CurrentTPStore.js
import _ from 'lodash';

import { makeObservable, withState } from 'utils';
import { apiRoutes, request } from 'api';

@makeObservable
export class CurrentTPStore {
  /**
   * @param rootStore {RootStore}
   */
  constructor(rootStore) {
    this.rootStore = rootStore;
    this.executions = {};
  }

  @withState
  fetchSymbol() {
    return request(apiRoutes.symbolInfo)
      .then(this.fetchSymbolSuccess)
      .catch(this.fetchSymbolError);
  }
  fetchSymbolSuccess(data) {
    return Promise.resolve();
  }
  fetchSymbolError(error) {
    console.error(error);
  }
}

src/components/TestComponent.js
import React from 'react';

import { observer } from 'utils';
import { useStore } from 'hooks';

function TestComponent() {
  const store = useStore();
  const { currentTP: { executions } } = store;

  return <div>{executions.fetchSymbol ? 'Загружается...' : 'Загружен'}</div>;
}

export const TestComponentConnected = observer(TestComponent);

Вроде полностью соответствует вашему описанию, только в виде объекта с ключами выполняемых асинхронных действий. Это выгоднее, так как позволяет обновлять компонент только при обновлении стейта конкретного действия — если же использовать массив (loaders.indexOf('fetchSymbol') !== -1), компонент будет обновляться и при попадании в этот массив всех других, ненужных для данного компонента стейтов. Хотя в Redux можно сделать селектор для этого. Глобальный лоадер можно завязать на Object.entries(executions).some(([key, value]) => value === true).


Разумеется, на любом движке или ванильном JS можно сделать то же самое, просто на MobX это очень просто и удобно.

Присоединяюсь к «выводу что react + mobx серебряная пуля для задач и проектов любой сложности» — задачи, стоящие перед frontend-разработкой решаются насколько элегантно, что не приходится раскручивать цепочки вида компонент-родительский компонент-контейнер-селектор-экшен-тип экшена-поиск затрагиваемых сторов-обработчик АПИ запросов-нормализатор, которые присутствуют в более-менее сложных проектах на Redux. Практически ничто не мешает свободному полету мысли и созданию качественного приложения. Разве что Typescript я бы все же не прикручивал — мороки намного больше, чем профита, по моему опыту.
JSX — просто калька с того, как написаны php-сайты 2000х

Я бы не был так категоричен, по моему мнению это лучший на сегодня шаблонизатор, максимально приближенный к html. В отличие от Vue и Angular, где в кастомных атрибутах в виде строк описывается логика работы — подход, с которым сообщество борется десятки лет, но он почему-то жив и приобретает новые формы.


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

На моей практике стандарты кодирования и стандарты качества (неинструментальные, но все же ограничения) способствуют формированию общности в среде разработчиков, если создаются демократическими методами. ESLint туда же. Не в одном проекте видел достаточно образованных разработчиков, которые в одной кодовой базе использовали кардинально разные подходы — с типизацией/без, функциональный стиль/ООП, thunk/saga, даже различные методы именования переменных. Нельзя сказать, что код каждого был плохой — но при уходе разработчика другим приходилось переписывать то, что он делал, так как не понимали его код.


В остальном согласен, разве что только не MobX обновляет UI, а все же React в данном случае, просто эффективней (хотя крайне мало проектов, в которых важно — обновилось 10 компонентов или 1)

Исправил в апдейте на кроссплатформенный, только \/ будет недостаточно, а [\\/] покроет оба написания слэшей. Папка dist коммитится для демо — так работают Github Pages, они показывают контент одной из папок в репозитории. Другие хеши в первоначальном варианте получились скорее всего потому, что скрин с ними я сделал до финальной версии кода — так что если бы и сам пересобрал, то были бы другие. С yarn.lock разобрался в апдейте, действительно было ошибкой не использовать его для основных зависимостей
При проработке названия искал слово, которым можно емко объединить сразу много сфер — контроль качества и форматирования кода, сборку, оптимизацию, работу со стилями, метод организации файлов, создание связки store+view, локализацию, практический обзор работы с React Hooks, принципы работы с АПИ, проектирование роутинга, конечное тестирование. Можно это было бы назвать «Основа для полноценного SPA-приложения», но тоже можно найти недостатки.

Так как «архитектура ПО» — размытое понятие, у которого сотни определений с разным охватом тем, можно взять например такое от Екатерины Головановой: «это процесс превращения таких характеристик программного обеспечения, как: гибкость, масштабируемость, возможность реализации, многократность использования и безопасность — в структурированное решение, которое соответствует как техническим, так и бизнес требованиям». По большей части подходит, на мой взгляд.

Понимаю, что тем, кто хотел увидеть в статье архитектуру «в узком смысле» — как «выбор структурных элементов, интерфейсов и их поведения в рамках системы» — здесь информации не много, так что для них это выглядит кликбейтом.

Boilerplate — «шаблонный код, который должен быть написан во многих местах практически без изменений», в статье встречается только в виде

  /**
   * @param rootStore {RootStore}
   */
  constructor(rootStore) {
    this.rootStore = rootStore;
  }

и параметров вроде «name», «version» в *.json файлах. А как тогда назвать все остальное? Архитектура вроде как подходит
Сейчас пришла идея, что если потребуется изменить в одной транзакции сразу много сторов, можно это красиво сделать через реализованный в статье autorun. Таким образом, сторы потеряют тесную связь, что снизит сложность программы. Да, так всегда — по мере «выращивания» кода приходят идеи, как его улучшить. Сразу идеально спроектировать у меня никогда не получалось
У меня скорее негативный опыт взаимодействия с TypeScript. Понимаю, что тема спорная, и зависит от принципов, которых придерживается сам разработчик. Знаю сильных специалистов, которым комфортно работать в стиле «тише едешь — дальше будешь», и проекты они выполняют с полной тщательной типизацией и за тройной, на мой взгляд, срок, при сравнимом качестве итогового продукта.

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

Сам код, на мой взгляд, становится сложнее читаемым, в миддлварах и сложных функциях и декораторах приходится писать многотомные типы ценой дополнительных часов, не получая профита (автодополнение? JSDoc в помощь), а ввиду того, что работал в проектах, где важна скорость разработки, приходилось «скатываться» в any. Еще помню большие затраты времени на обучение корректному использованию типизации всех разработчиков. Для динамической типизации в ряде случаев приходилось использовать хаки со StackOverflow, также было несколько проблем касательно утечек памяти при сборке (watch-режим падал, какие бы лоадеры и ухищрения не пробовали, через 2-3 часа работы), увеличении времени на сборку, и баги, специфичные для Linux / Windows систем.

В итоге польза была катастрофически не совместима с затрачиваемыми усилиями. Эти минусы касаются именно SPA-разработки. Для отдельных библиотек (к примеру, для графиков) и онлайн-игр, думаю, типизация была бы очень кстати. А в разработке сайтов минимизирую количество багов тщательными интеграционными тестами и проверкой данных в рантайме.

Опыт работы с TypeScript — 6 месяцев. Итоговое впечатление — сырой продукт, не приносящий ощутимой пользы, при этом значительно увеличивающий время на разработку, при этом расхолаживая коллег, которые вместо заботы над реальным user experience начинают полагаться на «скомпилировалось — значит все будет работать».
Корневой стор передается, чтобы MobX-сторы имели доступ друг к другу и могли напрямую получать данные друг от друга. Всегда будут методы, которые должны изменить сразу несколько сторов — это просто решение проблемы. Диспетчер как в Redux здесь будет скорее неэффективен, так как желательно единомоментно в одной транзакции менять все параметры. А вы как решаете эту задачу?

Чтобы продемонстрировать принципы, которыми руководствуюсь, полезней и понятней написать свою реализацию, хотя можно было бы и сделать на базе готового инструмента, основательно обработав напильником.
Раньше использовал вышеуказанные библиотеки, но в большинстве проектов приходилось их клонировать и дорабатывать, либо использовать сложные конструкции, для соответствия тем принципам, которых я придерживаюсь при разработке.
react-intl, к примеру, работает через свой провайдер, требует в каждом сообщении передавать уникальный id (который, конечно, можно сгенерировать через _filename), не всегда подходит его шаблонизатор. Если можно сделать намного проще и удобней — то стараюсь избегать сторонних зависимостей.
react-router использовал 2-4 версии, каждый раз приходилось писать много лишнего кода, чтобы добиться тесной интеграции с приложением, вплоть до нескольких миддлвар на каждый onEnter / onLeave, а правами на просмотр управлять через Prompt… Работало хорошо, но код совсем не элегантный.
Create React App все же для небольших приложений, так как накладывает жесткие ограничения, в которые проект неминуемо упрется. Придется делать npm run eject и основательно перерабатывать конфиги (проходил, не удовольствие).

Уверен, что можно так обернуть готовые библиотеки, чтобы соответствовали всем предъявляемым требованиям — просто времени и усилий лично у меня займет больше, чем написать с чистого листа.
Верно, глубокие зависимости без lock-файла не будут зафиксированы. По всей видимости я сам себе противоречу в статье — выступаю за стабильные зависимости без ^, при этом допускаю возможность поломки из-за глубоких зависимостей.
Проблема, которую я решал — чтобы зависимости, указанные с помощью «file:./webpack-custom» обновлялись, так как с lock-файлом при изменении их package.json они не обновляются. Выходом вижу использовать yarn install --force. Возможно есть другие варианты?
12 ...
18

Information

Rating
6,567-th
Location
Москва, Москва и Московская обл., Россия
Date of birth
Registered
Activity