Я использую GraphQL с первого выпуска Vue-Apollo для каждого проекта разработки API, и, как и в любой другой новой рабочей среде, у Vue-Apollo есть свои недостатки. Один из них заключается в том, что вы не можете просто обновить собственное хранилище, не вызывая мутацию (на практике, фактически, запрос также обновляет хранилище, но это не относится к этой теме).
Очевидно, с девелоперской стороны это вполне логично. Если вы хотите сохранить свою концепцию SSOT, то это правильный путь.
Но, и всегда есть "но", в некоторых случаях может потребоваться обновить полученные данные GraphQL (в данном контексте store) на экране клиента. В моем случае вот сценарий:
- На стороне клиента есть простой ввод, и пользователь вводит некоторые номера Id в базу данных,
- На той же странице есть список «Последние номера идентификаторов», чтобы пользователь мог увидеть некоторые результаты после отправки данных,
- Когда страница загружается в первый раз, последние 10 номеров идентификаторов, полученных с помощью запроса GraphQL,
- Когда пользователь отправляет новый номер идентификатора, срабатывает мутация и обновляет запрос новыми данными,
- На стороне сервера команда Laravel работает каждую минуту, чтобы проверить, не помечен ли номер идентификатора как «недействительный»,
- Для этих номеров идентификаторов существует сторонний сервер проверки, и эта команда запускает задание для отправки данных номера идентификатора для проверки,
- А пока, чтобы показать пользователю, какова ситуация с этим процессом проверки, не обновляя его страницу или не опрашивая сервер GraphQL каждую секунду, я использую службу Pusher и что задание запускает событие для обновления списка «Последние номера идентификаторов», и начинается самое интересное,
- Хотя пользователь все еще может вставлять новые номера Id в базу данных, он все еще может видеть, что его список заполняется данными, которые он вводит, и данными, которые Pusher обновляет.
Проблема с этой концепцией в том, что вы можете обновить данные Vue (полученные из запроса GraphQL), и ваша страница все равно будет в полном порядке. На номер 8 выше Pusher отправляет обновление по своему каналу и сообщает об обновленных данных:
mounted () { this.channel = this.$pusher.subscribe(`private-User.Entries.${this.user.id}`) this.channel.bind('pusher:subscription_succeeded', (e) => { console.warn('Subscribed to Entries channel.') }) this.channel.bind('App\\Events\\EntryStatus', (data) => { this.onUpdateEntry(data) }) }
В этом коде вы можете видеть, что всякий раз, когда событие EntryStatus запускается на стороне Laravel, оно обновляет Pusher, а Pusher обновляет мое приложение Vue, а затем запускает метод onUpdateEntry с его обновленными данными.
data () { return { pendingUpdates: [] } }, methods: { onUpdateEntry (entryUpdated) { // First part const entryRendered = this.$apolloData.data.entriesPaginate.find(entry => entry.id === entryUpdated.id) if (entryRendered !== undefined) { entryRendered.status = entryUpdated.status } // Second part const index = this.pendingUpdates.findIndex(entry => entry.id === entryUpdated.id) if (index !== -1) { this.$set(this.pendingUpdates, index, entryUpdated) } else { this.pendingUpdates.push(entryUpdated) } }
Этот код обновляет обработанные данные GraphQL и одновременно опрашивает обновленные данные записи. Обратите внимание на объект entryRendered. Вы можете задаться вопросом, зачем кому-то опрашивать данные entryUpdated, если можно обновить apolloData, верно? Дело в том, что apolloData - это не настоящий магазин Apollo. Это набор данных, который вы можете изменить в методе результатов вашего запроса.
data () { return { pendingUpdates: [], loading: false, total: 0, pagination: { age: 1, rowsPerPage: 10, sortBy: 'id', descending: true }, entriesPaginate: [] } }, apollo: { entriesPaginate: () => ({ query: ENTRY_PAGINATE, context: { uri: 'http://localhost/graphql' }, variables () { return this.pagination }, result ({ data: { entriesPaginate: { data, total } }, loading }) { if (!loading) { this.total = total this.entriesPaginate = data.slice().map((key, i) => ({ ...key, __index: i })) } }, watchLoading (isLoading) { this.loading = isLoading } }) }
С Vue-Apollo вы можете игнорировать метод результата в своих запросах (а также массив entryPaginate в данных) и позволить Apollo обрабатывать присвоение результата запроса данным Vue. Но в моем случае мне понадобились общие данные отдельно, и в наборе данных могут быть другие изменения. Поэтому здесь я использую метод результата. Но использовать это или нет, массив EntriesPaginate будет частью исходного результата запроса магазина Apollo.
Поэтому не имеет значения, обновляю ли я this. $ ApolloData.data.entriesPaginate или this.entriesPaginate в методе onUpdateEntry. Когда пользователь отправляет новый идентификатор и запускает мутацию, Apollo запускает сам метод результата запроса (определенный или нет), а массив this.entriesPaginate обновляется исходными данными хранилища Apollo. Это означает, что все обновления из метода onUpdateEntry будут отменены на экране пользователя. Он / она увидит новую запись идентификатора и данные старого запроса.
Чтобы решить эту проблему, мы бы использовали метод this. $ Apollo.store, но его нет. Так что я придумал обходной вариант опроса обновленных данных. Вы можете заметить, что в методе onUpdateEntry есть две части кода с комментариями, первая и вторая. Вторая часть вставляет данные updatedEntry в массив pendingUpdates. Я сохраняю эти записи, чтобы сначала обновить хранилище, когда пользователь отправляет новый идентификатор в базу данных через мутацию. Вот:
data () { return { graphQLErrors: [], form: { idNumber: null }, pendingUpdates: [], // Empty here for definition, actual data filled with multiple entryUpdated data. } }, methods: { submit () { this.loading = true this.$apollo.mutate({ mutation: ENTRY_CREATE, variables: { entry: { id_number: this.form.idNumber } }, context: { uri: 'http://localhost/graphql' }, update: (store, { data: { entryCreated } }) => { const query = { query: ENTRY_PAGINATE, variables: this.pagination }, data = store.readQuery(query) if (data !== undefined) { // First part if (this.pendingUpdates.length) { this.pendingUpdates.forEach(entryUpdated => { const index = data.entriesPaginate.data.findIndex(entry => entry.id === entryUpdated.id) if (index !== -1) { this.$set(data.entriesPaginate.data, index, entryUpdated) } }) this.pendingUpdates = [] // Reset } // Second part if (data.entriesPaginate.data.findIndex(entry => entry.id === entryCreated.id) === -1) { data.entriesPaginate.data.unshift(entryCreated) store.writeQuery({ ...query, data: data }) } } } }).catch((e) => { this.graphQLErrors = e }).then(() => { this.loading = false }) } }
Вы можете заметить, что здесь метод update Мутации снова состоит из двух частей. Первая часть перед обновлением хранилища вновь созданными данными выполняет итерацию по массиву pendingUpdates и обновляет хранилище Apollo, затем вторая часть вставляет объект entryCreated в начало list и, наконец, store.writeQuery завершает обновление.
Самая важная часть из всего вышесказанного заключается в том, что вы должны опубликовать те же данные GraphQL в Pusher. Я не исследовал, способен ли мой пакет Laravel GraphQL на это, поэтому я просто имитирую то, как GraphQL обслуживает данные. Чтобы быть более понятным, это пример результатов запроса GraphQL для entriesPaginate:
{ data: { entriesPaginate: [ {id: 1, id_number: 12345678, created_at: SomeDate, validated_at: SomeDate, status: "queued", __typename: "Entry"}, {id: 2, id_number: 34567890, created_at: SomeDate, validated_at: SomeDate, status: "validated", __typename: "Entry"}, {id: 3, id_number: 45678901, created_at: SomeDate, validated_at: SomeDate, status: "validating", __typename: "Entry"}, ... ], total: 41, __typename: "entriesPagination" } }
И этот метод broadcastWith моего широковещательного события для отправки данных в Pusher:
public function broadcastWith() { return [ '__typename' => 'Entry', 'id' => $this->entry->id, 'id_number' => $this->entry->id_number, 'status' => $this->entry->status, 'created_at' => $this->entry->created_at, 'validated_at' => $this->entry->validated_at ]; }
Сохранение данных в том же формате предотвращает ошибки обновления магазина Apollo.
В примерах кода Vue я использовал наиболее релевантные элементы данных в объекте данных. Поэтому, пожалуйста, не пытайтесь использовать приведенный выше код для простого копирования и вставки, поскольку в них могут отсутствовать определения / методы данных.