Приложения без сохранения состояния и декларативные запросы ресурсов

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

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

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

Redux - популярная библиотека для реализации этого паттерна. У него отличный набор инструментов разработчика, и в сочетании с ImmutableJS для неизменяемых обновлений он также может быть весьма производительным. Redux в значительной степени вдохновлен Архитектурой Вяза, и я очень рекомендую прочитать об Вяз.

Мы наблюдаем тенденцию к тому, что наши программы становятся безгражданскими. То есть все состояние приложения хранится в одном сериализуемом объекте, а не распространяется на локальные переменные. Но часть наших программ по-прежнему сохраняет состояние, и это связано с извлечением данных.

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

fetchData: function(query) {
  this.setState({loading: true})
  fetch(url, {method: ‘get’})
    .then((response) => response.json())
    .then((data) => this.setState({data, loading: false}))
}

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

componentWillMount: function() {
  this.sub = Meteor.subscribe(‘chatroom’, this.props.roomId)
},
componentWillUnmount: function() {
  this.sub.stop()
}

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

function newTodo(text) {
  var todo = document.createElement(“span”)
  todo.textContent = todo
  document.getElementById(‘todos’).appendChild(todo)
}

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

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

Когда фактический побочный эффект (получение данных) перемещается за пределы компонента React, мы можем начать использовать некоторые интересные уловки. Мы можем дедуплицировать запросы и кэшировать запросы или подписки с помощью простых функций высокого порядка. И вместо того, чтобы отправлять 50 отдельных HTTP-запросов на сервер, мы можем объединить их в один и отправить все сразу.

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

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

На этом этапе мы обнаружили именно мотивацию для GraphQL. Но вместо того, чтобы останавливаться на достигнутом и довольствоваться GraphQL, давайте еще немного подумаем об этой проблеме.

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

Используя этот шаблон, мы должны иметь возможность декларативно определять запросы домена и поддомена каждого компонента, во многом так же, как мы делаем с виртуальной DOM, и составлять их вместе в эффективные запросы к базе данных. Итак, предположим, что мы создаем монаду, которая представляет запросы домена и запросы субдоменов в некоторой точке иерархии компонентов. Пустой запрос выглядит так:

{ domain: [], subdomain: [] }

Компонент чата может иметь запрос домена верхнего уровня, поэтому он вернет что-то вроде этого:

{ 
  domain: [
    [‘chatroom’, {roomId: 42}, [
      [‘id’, {}, []],
      [‘name’, {}, []]
    ]]
  ],
  subdomain: []
}

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

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

{ 
  domain: [],
  subdomain: [
    [‘members’, {}, []],
    [‘messageCount’, {}, []]
  ]
}

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

{ 
  domain: [
    [‘chatroom’, {roomId: 42}, [
      [‘id’, {}, []],
      [‘name’, {}, []],
      [‘members’, {}, []],
      [‘messageCount’, {}, []]
    ]]
  ],
  subdomain: []
}

Теперь рассмотрим подкомпонент сообщений. Его запрос ресурса выглядит так.

{ 
  domain: [],
  subdomain: [
    [‘messages’, {limit: 50, skip: 0}, [
      [‘id’, {}, []],
      [‘text’, {}, []]
    ]]
  ]
}

Также может быть подкомпонент пользователя для отображения имени пользователя и изображения профиля рядом с каждым сообщением.

{ 
  domain: [],
  subdomain: [
    [‘owner’, {}, [
      [‘id’, {}, []],
      [‘name’, {}, []]
      [‘image_url’, {size: ‘sm’}, []]
    ]]
  ]
}

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

{ 
  domain: [
   [‘chatroom’, {roomId: 42}, [
     [‘id’, {}, []],
     [‘name’, {}, []],
     [‘members’, {}, []],
     [‘messageCount’, {}, []],
     [‘messages’, {limit: 50, skip: 0}, [
       [‘id’, {}, []],
       [‘text’, {}, []]
       [‘owner’, {}, [
         [‘id’, {}, []],
         [‘name’, {}, []]
         [‘image_url’, {size: ‘sm’}, []]
       ]]
     ]],
   ]]
  ],
  subdomain: []
}

Теперь предположим, что нам также нужен список чатов на боковой панели. У этого компонента будет другой запрос домена, например:

{ 
  domain: [
    [‘chatrooms’, {limit: 20, skip: 0}, [
      [‘id’, {}, []],
      [‘name’, {}, []],
      [‘members’, {}, []],
      [‘messageCount’, {}, []], 
    ]]
  ],
  subdomain: []
}

И когда мы визуализируем компонент чата вместе с компонентом списка чатов, мы можем просто объединить запросы домена.

{ 
  domain: [
    [‘chatroom’, {roomId: 12}, [
      [‘id’, {}, []],
      [‘name’, {}, []],
      [‘members’, {}, []],
      [‘messageCount’, {}, []],
      [‘messages’, {limit: 50, skip: 0}, [
        [‘id’, {}, []],
        [‘text’, {}, []]
        [‘owner’, {}, [
          [‘id’, {}, []],
          [‘name’, {}, []]
          [‘image_url’, {size: ‘sm’}, []]
        ]]
      ]],
    ]],
    [‘chatrooms’, {limit: 20, skip: 0}, [
      [‘id’, {}, []],
      [‘name’, {}, []],
      [‘members’, {}, []],
      [‘messageCount’, {}, []], 
    ]]
  ],
  subdomain: []
}

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

Теперь мы эффективно извлекли все требования нашего приложения к извлечению данных из пользовательского интерфейса в сериализуемый запрос. Если мы имеем дело с HTTP-запросами или каким-либо сторонним API, мы можем просто отобразить поток этих запросов (так же, как мы отображаем поток состояний с Redux) и выполнять все подписки, отмены подписки и императивные мутации, которые нам нужно за пределами пользовательского интерфейса.

Но если мы создаем приложение с нуля, почему бы просто не использовать JSON diff / patch для синхронизации этого объекта запроса с сервером через веб-сокеты и позволить серверу управлять всеми подписками? Тогда у нас было бы приложение без гражданства!

Я думаю, что это правильное направление, но декларативная выборка - это только четверть битвы. Следующая задача - преобразовать этот формат запроса в запрос к базе данных, который мы действительно можем выполнить. Учитывая рекурсивный характер этого формата, вы могли бы подумать, что это будет легко, но на самом деле это оказалось довольно сложной задачей. для меня и в результате заинтересовал меня Datomic.

Я не очень разбираюсь в базах данных, поэтому мне бы хотелось услышать отзывы о том, как мы можем анализировать эти рекурсивно вложенные запросы в запрос для выбранной вами базы данных, особенно если вас зовут Рич Хикки.

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

Falcor очень похож на GraphQL в том, что он пытается представить данные JSON в виде графика. Falcor имеет концепцию наборов путей для декларативной выборки данных, но GraphQL, похоже, уделяет гораздо больше внимания составлению запросов с компонентами React через Relay.

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

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

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

Наконец, я подумал, что заранее отвечу на надвигающийся вопрос:

Почему бы просто не использовать Falcor или GraphQL?

Это хороший вопрос. Вы, конечно, можете! Я просто думаю, что они могут быть лучшим решением. Одна проблема, с которой я столкнулся с Falcor и GraphQL, заключается в том, что они зависят от своего собственного предметно-ориентированного языка (DSL). Falcor имеет свой синтаксис для наборов путей, а GraphQL имеет собственный синтаксис полностью. И я не говорю, что синтаксис сложен для понимания - мне действительно очень нравится синтаксис GraphQL.

В первую очередь, мне не нравится, как эти библиотеки предпочитают не использовать примитивы JSON. Это сильно затрудняет кому-то другому проникнуть туда и взломать, потому что я не хочу анализировать все эти струны!

Другая, более важная вещь, о которой я действительно хочу сказать в этой статье, - это то, что все это относительно простые шаблоны программирования. GraphQL - отличная идея и на самом деле это просто модная монада!