Добро пожаловать назад! Если вы новичок в нашем путешествии и у вас нет установленного API GraphQL, я бы порекомендовал вам начать здесь, где мы настраиваем наш бэкэнд GraphQL в Laravel с пакетом Lighthouse. Если вы просто хотите пройти через интерфейс, прокрутите вниз до Установить Vue Apollo и посмотрите пример репозитория в этой фиксации.

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

Когда мы закончили, мы были заняты сиянием нашего нового блестящего API GraphQL: запрашивали продукты и shoppingCarts и добавляли первое ко второму. Но, каким бы прекрасным ни был graphiql, мы, вероятно, не хотим доставлять этот интерфейс напрямую конечному пользователю ... Не так ли? Нет, наверное, нет. Итак, давайте интегрировать интерфейс с нашим API!

Как и раньше с Laravel, я предполагаю, что у вас есть некоторые знания VueJS. Если вы этого не сделаете, вы, скорее всего, будете немного ошеломлены, поскольку я ввожу все компоненты за один прием, чтобы сэкономить нам время на экспозицию. Если вы не знакомы, Vue - отличный фреймворк для JavaScript. Я бы рекомендовал начать с документации VueJs. Если вы частый пользователь Laravel, в Laracasts также есть несколько отличных руководств по Vue начального уровня, в которых Laravel используется в качестве отправной точки.

Все здесь, кто хочет оставаться здесь? Отлично, давайте установим кое-что.

Я собираюсь предположить, что все, кто здесь находится, пришли из моего последнего руководства, но я попытаюсь указать на незначительные отличия, которые вы можете увидеть, если не используете настройку Vue, которая поставляется вместе с Laravel. Все это будет работать так же хорошо, если вы запустили Vue через невероятно отличный Vue CLI v3 UI (мой любимый мир). Если вы используете yarn вместо npm, это также будет работать нормально, просто заменив ваши обычные команды yarn вместо команд npm, которые я использую.

Установите Vue-Apollo fa6a2da2ccfb8a74a6502892d460f418b3530cbd

Во-первых, если вы еще не запустили npm install в своем проекте, сделайте это сейчас.

Теперь давайте установим несколько пакетов.

npm install vue-apollo apollo-boost graphql graphql-anywhere

Vue Apollo - это пакет, который нам действительно нужен, и он также требует apollo-boost и graphql в качестве конфигурации по умолчанию. Graphql-anywhere - еще один отличный пакет, полный служебных функций, которые позже помогут нам с нашими фрагментами запросов. Вы узнаете это, когда мы немного импортируем метод filter.

Настроить Vue Apollo dcde8ed217e660f2b8def0041658f2f672008844

Вся необходимая конфигурация будет выполняться в resources/js/app.js. Если вы не используете Laravel, они будут работать везде, где вы создаете экземпляр Vue. Если сомневаетесь, проверьте документацию Vue Apollo. Нам нужно будет импортировать Apollo, немного настроить его и присоединить к нашему экземпляру Vue. Это, вероятно, легче всего увидеть по разнице, поэтому давайте посмотрим на это:



Теперь, когда мы все настроены, давайте получим данные с сервера!

Настройка страницы корзины покупок 3f5b4b733183dab07e57c1f1823f495cd6afd245

Давайте начнем с создания нового представления в resources/views/shopping-cart.blade.php, которое будет нашей точкой входа в наше приложение Vue. Он унаследует макет нашего приложения и просто отобразит наш основной компонент. Мы также добавим к нему соответствующий маршрут.

# routes/web.php
Route::view('/shopping-cart', 'shopping-cart');
# resources/views/shopping-cart.blade.php
@extends('layouts.app')
@section('content')
<div class="container">
    <div class="row justify-content-center">
        <shopping-cart-page />
    </div>
</div>
@endsection

Для тех из вас, кто не работает в Laravel, это будет просто ваш основной компонент.

Если мы перейдем по этому маршруту в нашем браузере на /shopping-cart, мы получим предупреждение консоли о незарегистрированном компоненте. Это правильно! Итак, давайте создадим наш компонент и зарегистрируем его. Давайте запустим npm run watch, чтобы мы могли писать javascript, не думая о переносе, и заменим example-component, зарегистрированный в resources/js/app.js, на компонент, который мы назовем ShoppingCartPage.

У нас должна быть строка кода в resources/js/app.js, которая выглядит так:

Vue.component('shopping-cart-page', require('./components/ShoppingCartPage.vue'));

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

Вот наш первый компонент!

Давайте разберемся немного, начнем с запроса.

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

Этот запрос передается в метод gql, который преобразует нашу строку в узлы graphql для чтения сервером. Когда я впервые увидел этот синтаксис, я был очень сбит с толку и потратил много времени, придумывая правильные слова для Google. Оказывается, это слова “tagged template literals” , если вы хотите узнать больше, но суть их использования здесь в том, что вы можете создавать функции, которые принимают строки с обратными кавычками и используют этот синтаксис.

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

Если вы еще не видели $options в шаблоне, он позволяет нам получать данные непосредственно из экспортированного объекта Vue. Хранение данных здесь дает нам два преимущества: Vue не тратит свои ресурсы на настройку перехватчиков реактивности для данных, которые не изменяются, И это позволяет нам получать доступ к этим свойствам извне компонента.

scopedSlot передает нам объект с полем result, содержащим другой объект с data, loading и error, которые не требуют пояснений.

Если вы не знакомы с компонентами более высокого порядка и scopedSlots, я бы порекомендовал как Документы Vue, так и Труды Адама Уотана по этой теме. Суть этого для наших целей состоит в том, что это slot компонент упаковки может предоставлять данные.

Затем мы берем эти значения и просто выгружаем data на экран, как только loading станет false.

Если мы сейчас выберем маршрут (для меня это http://laravel-vue-graphql-tutorial.test/shopping-cart), мы должны увидеть какие-то сброшенные данные!

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

Стиль и компоненты It Up aa6c04c8a2193697b356ba582eec345596820cbf

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

Давайте разберем приложение на компоненты и (с помощью моей жены @heatherakshar) немного их стилизуем!

Вот общая структура:

А вот как это выглядит сейчас!

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



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

Если вы не знакомы с использованием v-bind, он просто разрушает объекты и передает их как отдельные свойства. Так сказать <component :name=”person.name” :age=”person.age” /> может быть то же самое, что сказать <component v-bind="person" />.

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

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

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

Преобразовать во фрагменты: ba7126f3c755cc642a29f403d9758d4226774d94

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

Начнем с определения части запроса, которой занимается ProductsList (выделено жирным шрифтом)

query ShoppingCartPage {
    me {
        id
        shoppingCart {
            id
            total
            totalBeforeSavings
            savings
            products {
                id
                name
                price
                savings
                quantity
            }
        }
    }
    products {
        id
        name
        price
        savings
    }            
}

Теперь мы можем представить его как фрагмент на ProductsList.vue:

...
<script>
    import gql from 'graphql-tag';
    ...
    export default {
        ...
        fragment: gql`
            fragment ProductsList on Query {
                products {
                    id
                    name
                    price
                    savings
                }
             }
        `,
    }
</script>
...

Давайте разберем то, что мы только что добавили. Подобно нашему запросу, который мы написали ранее, мы добавляем ключ фрагмента прямо к нашему экспортированному объекту Vue. Значение этого ключа - строка, передаваемая в функцию gql. Вместо ключевого слова query для его запуска мы использовали ключевое слово fragment. Имя ProductsList, как и раньше, совпадает с именем компонента, в котором оно находится (по соглашению). on Query - это необходимый бит синтаксиса, который позволяет фрагменту узнать, что это за родительский компонент. Поскольку мы помещаем его непосредственно в базовый запрос GraphQL, мы используем Query.

Теперь ProductsList может диктовать, какие данные GraphQL ему нужны для правильной визуализации. Мы можем включить этот фрагмент в наш основной запрос, заменив выделенный жирным шрифтом сегмент, в котором мы идентифицировали запрос, на …ProductsList, а затем интерполировать фрагмент в нижней части строки запроса (после закрывающего }, но перед закрывающим обратным апострофом на ${ProductsList.fragment}. Вот что новый запрос должен выглядеть так:

query: gql`
    query ShoppingCartPage {
        me {
            id
            shoppingCart {
                id
                total
                totalBeforeSavings
                savings
                products {
                    id
                    name
                    price
                    savings
                    quantity
                }
            }
        }
       ...ProductsList          
   }
   ${ProductsList.fragment}
`

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

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

Мы можем ввести дополнительный фрагмент в ProductButton, который будет относиться к типу Product (вместо предыдущего типа запроса), который указывает, какие отдельные поля нам нужны.

fragment ProductButton on Product {
    id
    name
    price
    savings
}

Затем мы можем импортировать этот фрагмент в ProductsList, чтобы контейнер ShoppingCart по-прежнему получал весь запрос. Теперь мы куда-то идем!

// In ProductsList.vue
fragment: gql`
    fragment ProductsList on Query {
        products {
             id
             ...ProductButton
        }
     }
     ${ProductButton.fragment}
`,

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

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

И последнее, что мы должны сделать здесь, как лучшая практика. Предположим, у нас есть два компонента, например PersonDetails и PersonButton. У каждого из этих компонентов есть фрагмент, который запрашивает некоторые данные из типа Person, который запрашивается у их общего родителя, PersonContainer. PersonDetails запрашивает и name, и age, а PersonButton запрашивает только name. Как мы знаем, оба этих фрагмента передаются в основной запрос родительского запроса, он запускается один раз, а затем родительский v-binds возвращаемый объект person для каждого компонента. Довольно простой вариант использования.

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

Чтобы предотвратить эту ситуацию, мы можем использовать filter из библиотеки graphql-anywhere. Filter - это метод, который принимает два аргумента: фрагмент и объект данных, а затем возвращает ТОЛЬКО то, что запрашивал фрагмент. С фильтром нам никогда не нужно беспокоиться о передаче правильных данных, углублении в возвращаемые объекты или использовании какого-либо поля, которое мы не запрашивали! Давайте посмотрим, насколько приятнее это делает наши утверждения v-bind.

В ShoppingCartPage нам просто нужно импортировать filter с import {filter} from ‘graphql-anywhere’;, зарегистрировать его в нашем компоненте с помощью: methods: {filter},, а затем заменить наш ProductsList v-bind на:

v-bind="filter($options.components.ProductsList.fragment, data)"

Немного многословно, так как нам нужно пройти через объект компонентов, но он дает неплохую функциональность! После импорта и регистрации в нашем компоненте ProductsList мы можем аналогичным образом заменить ProductButton v-bind на:

v-bind="filter($options.components.ProductButton.fragment, product)"

Если вы чувствуете себя немного сбитым с толку этой концепцией фрагментов, вы не одиноки. Я тоже был таким, когда меня впервые представили. Когда я начал его использовать, я быстро купил. Возможность совмещать требования к данным для каждого запроса внутри компонента, сохраняя при этом их как чистые презентационные компоненты и выполняя только один вызов API? Считай меня!

Давайте повторим этот шаблон для других наших компонентов! Если вы застряли, хотите получить подсказку или просто пропустите, посмотрите разницу здесь:



Теперь это становится довольно хорошо! Не хватает только одной концепции… наших мутаций! Знаете, это то, что заставляет наше приложение что-то делать!

Создание мутаций db21e2c44522308f75f0db8fa2e6d97aefecd95f

Мы находимся на финишной прямой в нашем приложении! Давайте добавим нашу addProductToShoppingCart мутацию в ProductButton, чтобы она могла добавить товар в наш список покупок. Мы собираемся использовать другой компонент оболочки, на этот раз под названием ApolloMutation, который принимает мутацию и некоторые переменные и предоставляет другой scopedSlot.

Давайте взглянем на наш новый компонент и рассмотрим некоторые изменения.

Мы начнем с самого начала, и первое, что вы, вероятно, заметите, - это новый компонент упаковки ApolloMutation. Как упоминалось выше, он принимает мутацию (которую мы определяем точно так же, как наш предыдущий запрос) и объект, полный переменных, которые нужно передать мутации. В нашем случае мы хотим передать идентификатор продукта как productId (поскольку это именно то, что ищет GraphQL). Не бойтесь снова обратиться к Graphiql, если вам нужно освежить в памяти нашу документацию по мутациям, вот для чего он нужен!

Затем компонент ApolloMutation предоставляет нам scopedSlot с объектом, содержащим ключ mutate и ключ loading (среди прочего, например error, но это те, которые нас интересуют). Клавиша mutate - это функция, которую следует вызывать всякий раз, когда мы хотим запустить мутацию. Ключ loading - это логическое значение, которое сообщает нам…. да вы, наверное, догадались, загружается ли мутация. Мы хотим активировать мутацию каждый раз, когда нажимают на нашу кнопку, и мы не хотим, чтобы люди нажимали, пока она загружается, поэтому мы можем просто добавить все эти свойства и события прямо к нашей кнопке!

Внизу в теге скрипта вы увидите, что мы импортировали некоторые компоненты и добавили мутацию. Сама мутация немного похожа на наш запрос, хотя в ней есть небольшая дополнительная церемония. Первая строка mutation productButton($productId: ID!) { называет нашу мутацию и поясняет ее сигнатуру, чтобы мы знали, какие переменные мы могли бы ей передавать. В этом случае, поскольку это обязательный параметр (обратите внимание на !), это переменная, которую мы должны передать. Далее следует фактическая мутация, такая же, как и в Graphiql, с использованием переменной $productId для представления данных, которые мы собираемся передать.

Наши полезные данные выглядят точно так же, как наши запросы, которые мы видели раньше, и знаете что? Здесь тоже пригодятся наши фрагменты! Благодаря фрагментам мы можем запрашивать именно те новые данные, которые нужны нашим презентационным компонентам.

Но какой в ​​этом смысл? И почему мы так осторожно добавляем поле id ко всему? И разве мы не должны генерировать событие, чтобы сообщить чему-то, что мы обновили и получили новые данные…?

Ого.

Как…

Какие?

Даже итоги?

Давайте разберемся, что только что произошло.

Если вы получили сообщение об ошибке консоли, например Unexpected token < in JSON at position 0, убедитесь, что вы все еще вошли в систему!

Apollo включает в себя нечто, называемое InMemoryCache. Это механизм кэширования в браузере, который Apollo использует, чтобы гарантировать, что нам не придется снова и снова обращаться к GraphQL API для одних и тех же данных. (Поскольку GraphQL использует POST для обработки своих данных, у него нет встроенного кэширования, как в REST.)

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

Каждый раз, когда вы возвращаете данные из мутации, которая включает как ID, так и по крайней мере те же поля, что и что-то в кеше, Apollo автоматически обновит кэшированные данные. Таким образом, используя наши фрагменты в полезной нагрузке мутации, Apollo знает, что нужно автоматически обновлять наши кэшированные shoppingCart данные возвращенными данными. Это обновление изменит данные в наших реквизитах и ​​вызовет повторный рендеринг Vue.

Если вы когда-либо сталкивались с постоянным излучением, попытками отслеживать состояние во время обновлений, делать дополнительные вызовы REST в целях защиты, перезагружать страницы с сервера и читать документы Vuex, задаваясь вопросом, подходит ли это вам (и приятно, как Vuex , надеюсь, что это не так!), я надеюсь, что это взорвало вам голову так же, как и мое.

И мы до сих пор не добавили какое-либо внутреннее состояние ни в один из наших компонентов.

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

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

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

Вот код этого последнего сегмента:



Я жду, потому что предполагаю (и надеюсь), что вы в настоящее время добавляете и удаляете продукты с расслабленным выражением лица. По крайней мере, именно так я поступил, когда впервые начал использовать GraphQL и Apollo.

Это приложение впечатляет внешне? Нет, не совсем. Что делает его впечатляющим, так это то, что вы знаете, как мало кода вы написали. Как ни один из ваших компонентов не имеет свойства данных для отслеживания состояния. Как мало вам нужно отслеживать от компонента к компоненту по мере развития этого приложения.

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

Я провожу много времени в мире React, где у вас есть выбор между компонентами с отслеживанием состояния или без него, и каждый из них имеет отдельный синтаксис, поэтому более очевидно, какой из них используется. Благодаря Apollo и GraphQL я теперь несколько удивлен, когда наткнулся на компонент с отслеживанием состояния, основанный на классах, и это касается всего производственного приложения SaaS. До? Я даже не знал, что в React есть компоненты без сохранения состояния, вот как мало я их использовал.

Для меня наличие компонентов без сохранения состояния (Vue или React) также требует некоторой дисциплины, чтобы поддерживать их в таком состоянии. Простые, одноцелевые компоненты (прямо как мы так долго практиковались на бэкенде!) - это хорошо! Легко рассуждать и использовать, легко отлаживать, легко тестировать.

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

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

Спасибо за чтение и огромное спасибо всем библиотекам с открытым исходным кодом и авторам, которые делают этот код возможным!

В первую очередь за это руководство спасибо: Evan You для VueJS, Guillaume CHAU для Vue Apollo (и пользовательскому интерфейсу Vue CLI, просто потому, что я хочу отметить, насколько велик это снова), Тейлор Отвелл для Laravel и Бенедикт Франке / Кристофер Мур для Lighthouse. И, конечно же, благодаря множеству пакетов и их участников, от которых зависят эти библиотеки.

Еще раз спасибо за чтение и найдите меня в твиттере по адресу: