كيفية إنشاء نسخة Yelp كاملة المكدس باستخدام React & GraphQL (الإصدار العالمي من الكثبان الرملية)

يجب ألا أخاف. الخوف هو قاتل العقل. الخوف هو الموت القليل الذي يجلب الطمس الكامل. سأواجه خوفي. سأسمح لها بالمرور فوقي ومن خلالي. وعندما يمر ، سأدير العين الداخلية لأرى مساره. حيث ذهب الخوف لن يكون هناك شيء. فقط سأبقى.

- "دعاء ضد الخوف" فرانك هربرت ، ديون

قد تتساءل ، "ما علاقة الخوف بتطبيق React؟" بادئ ذي بدء ، ليس هناك ما يدعو للخوف في تطبيق React. في الواقع ، في هذا التطبيق بالذات ، حظرنا الخوف. أليس هذا لطيفا؟

الآن بعد أن أصبحت جاهزًا لتكون بلا خوف ، دعنا نناقش تطبيقنا. إنها نسخة مصغرة من Yelp حيث بدلاً من مراجعة المطاعم ، يقوم المستخدمون بمراجعة الكواكب من سلسلة الخيال العلمي الكلاسيكية ، Dune. (لماذا؟ لأن هناك فيلم جديد على الكثبان الرملية سيصدر ... لكن بالعودة إلى النقطة الرئيسية.)

لبناء تطبيقنا الكامل ، سنستخدم التقنيات التي تجعل حياتنا سهلة.

  1. رد الفعل: إطار أمامي بديهي وتركيبي ، لأن أدمغتنا تحب تكوين الأشياء.
  2. GraphQL: ربما سمعت بالعديد من الأسباب التي تجعل GraphQL رائعة. إلى حد بعيد ، أهمها إنتاجية المطورين وسعادتهم .
  3. Hasura: قم بإعداد واجهة برمجة تطبيقات GraphQL يتم إنشاؤها تلقائيًا أعلى قاعدة بيانات Postgres في أقل من 30 ثانية.
  4. هيروكو: لاستضافة قاعدة البيانات الخاصة بنا.

و GraphQL تمنحني السعادة كيف؟

أرى أنك متشكك. ولكن من المرجح أنك ستأتي بمجرد قضاء بعض الوقت مع GraphiQL (ملعب GraphQL).

يعد استخدام GraphQL أمرًا في غاية السهولة لمطور الواجهة الأمامية ، مقارنة بالطرق القديمة لنقاط نهاية REST القديمة. تمنحك GraphQL نقطة نهاية واحدة تستمع إلى كل مشاكلك ... أعني الاستفسارات. إنه مستمع رائع لدرجة أنه يمكنك إخباره بما تريده بالضبط ، وسوف يمنحك إياه ، لا أقل ولا أكثر.

هل تشعر بالحماس تجاه هذه التجربة العلاجية؟ دعنا نتعمق في البرنامج التعليمي حتى تتمكن من تجربته في أسرع وقت ممكن!

؟؟ هذا هو الريبو إذا كنت ترغب في الترميز.

P الفن 1: S earch

S تيب 1: D eploy إلى Heroku

الخطوة الأولى في كل رحلة جيدة هي الجلوس مع بعض الشاي الساخن واحتسائه بهدوء. بمجرد القيام بذلك ، يمكننا النشر إلى Heroku من موقع Hasura. سيؤدي ذلك إلى إعدادنا لكل ما نحتاجه: قاعدة بيانات Postgres ومحرك Hasura GraphQL وبعض الوجبات الخفيفة للرحلة.

black-books.png

الخطوة 2: إنشاء جدول الكواكب

مستخدمينا يريدون مراجعة الكواكب. لذلك قمنا بإنشاء جدول Postgres عبر وحدة التحكم Hasura لتخزين بيانات كوكبنا. من الجدير بالذكر الكوكب الشرير ، Giedi Prime ، الذي لفت الانتباه بمطبخه غير التقليدي.

جدول الكواكب

في غضون ذلك ، في علامة التبويب GraphiQL: قامت Hasura بإنشاء مخطط GraphQL تلقائيًا! العب مع المستكشف هنا ؟؟

مستكشف GraphiQL

S فاتر 3: C تتفاعل reate التطبيق

سنحتاج إلى واجهة مستخدم لتطبيقنا ، لذلك نقوم بإنشاء تطبيق React وتثبيت بعض المكتبات لطلبات GraphQL والتوجيه والأنماط. (تأكد من تثبيت Node أولاً.)

> npx create-react-app melange > cd melange > npm install graphql @apollo/client react-router-dom @emotion/styled @emotion/core > npm start

S TEP 4: S وآخرون تصل أبولو العميل

سيساعدنا عميل Apollo في طلبات شبكة GraphQL والتخزين المؤقت ، حتى نتمكن من تجنب كل هذا العمل الشاق. نجري أيضًا استعلامنا الأول ونسرد كواكبنا! بدأ تطبيقنا في الظهور.

import React from "react"; import { render } from "react-dom"; import { ApolloProvider } from "@apollo/client"; import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; import Planets from "./components/Planets"; const client = new ApolloClient({ cache: new InMemoryCache(), link: new HttpLink({ uri: "[YOUR HASURA GRAPHQL ENDPOINT]", }), }); const App = () => (    ); render(, document.getElementById("root"));

نقوم باختبار استعلام GraphQL الخاص بنا في وحدة تحكم Hasura قبل نسخه ولصقه في الكود الخاص بنا.

import React from "react"; import { useQuery, gql } from "@apollo/client"; const PLANETS = gql` { planets { id name cuisine } } `; const Planets = ({ newPlanets }) => { const { loading, error, data } = useQuery(PLANETS); if (loading) return 

Loading ...

; if (error) return

Error :(

; return data.planets.map(({id, name, cuisine}) => (

{name} | {cuisine}

)); }; export default Planets;

S فاتر 5: S قائمة تيل

قائمة الكواكب لدينا رائعة وكل شيء ، لكنها تحتاج إلى القليل من التغيير باستخدام Emotion (انظر الريبو لمعرفة الأنماط الكاملة).

قائمة من الكواكب

S فاتر 6: S شكل earch والدولة

يرغب مستخدمونا في البحث عن الكواكب وترتيبها بالاسم. لذلك نضيف نموذج بحث يستعلم عن نقطة النهاية الخاصة بنا بسلسلة بحث ، ونمرر النتائج Planetsلتحديث قائمة الكواكب. نستخدم أيضًا React Hooks لإدارة حالة التطبيق لدينا.

import React, { useState } from "react"; import { useLazyQuery, gql } from "@apollo/client"; import Search from "./Search"; import Planets from "./Planets"; const SEARCH = gql` query Search($match: String) { planets(order_by: { name: asc }, where: { name: { _ilike: $match } }) { name cuisine id } } `; const PlanetSearch = () => { const [inputVal, setInputVal] = useState(""); const [search, { loading, error, data }] = useLazyQuery(SEARCH); return ( setInputVal(e.target.value)} onSearch={() => search({ variables: { match: `%${inputVal}%` } })} /> ); }; export default PlanetSearch;
import React from "react"; import { useQuery, gql } from "@apollo/client"; import { List, ListItem } from "./shared/List"; import { Badge } from "./shared/Badge"; const PLANETS = gql` { planets { id name cuisine } } `; const Planets = ({ newPlanets }) => { const { loading, error, data } = useQuery(PLANETS); const renderPlanets = (planets) => { return planets.map(({ id, name, cuisine }) => (  {name} {cuisine}  )); }; if (loading) return 

Loading ...

; if (error) return

Error :(

; return renderPlanets(newPlanets ; }; export default Planets;
import React from "react"; import styled from "@emotion/styled"; import { Input, Button } from "./shared/Form"; const SearchForm = styled.div` display: flex; align-items: center; > button { margin-left: 1rem; } `; const Search = ({ inputVal, onChange, onSearch }) => { return (   Search  ); }; export default Search;
import React from "react"; import { render } from "react-dom"; import { ApolloProvider } from "@apollo/client"; import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; import PlanetSearch from "./components/PlanetSearch"; import Logo from "./components/shared/Logo"; import "./index.css"; const client = new ApolloClient({ cache: new InMemoryCache(), link: new HttpLink({ uri: "[YOUR HASURA GRAPHQL ENDPOINT]", }), }); const App = () => (     ); render(, document.getElementById("root"));

S tep 7: B e فخور

لقد قمنا بالفعل بتنفيذ قائمة الكواكب وميزات البحث! نحن ننظر بحب إلى أعمالنا اليدوية ، ونلتقط بعض الصور الذاتية معًا ، وننتقل إلى المراجعات.

قائمة الكوكب مع البحث

ف الفن 2: المراجعات الحية

S تيب 1: C استعراض reate الجدول

سيقوم مستخدمونا بزيارة هذه الكواكب وكتابة مراجعات حول تجربتهم. نقوم بإنشاء جدول عبر وحدة التحكم Hasura لبيانات المراجعة الخاصة بنا.

جدول المراجعات

نضيف مفتاح خارجي من planet_idعمود إلى idعمود في planetsالجدول، تشير إلى أن planet_idالصورة ل reviewsديك لمباراة idالصورة ل planets.

مفاتيح خارجية

S فاتر 2: T الرف العلاقات

يحتوي كل كوكب على عدة مراجعات ، بينما لكل مراجعة كوكب واحد: علاقة واحد لكثير. نقوم بإنشاء هذه العلاقة وتتبعها عبر وحدة تحكم Hasura ، بحيث يمكن كشفها في مخطط GraphQL الخاص بنا.

تتبع العلاقات

الآن يمكننا الاستعلام عن مراجعات لكل كوكب في Explorer!

الاستعلام عن مراجعات الكوكب

S فاتر 3: S وآخرون تصل التوجيه

نريد أن نكون قادرين على النقر فوق كوكب وعرض مراجعاته في صفحة منفصلة. قمنا بإعداد التوجيه باستخدام React Router ، وقائمة المراجعات على صفحة الكوكب.

import React from "react"; import { render } from "react-dom"; import { ApolloProvider } from "@apollo/client"; import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; import { BrowserRouter, Switch, Route } from "react-router-dom"; import PlanetSearch from "./components/PlanetSearch"; import Planet from "./components/Planet"; import Logo from "./components/shared/Logo"; import "./index.css"; const client = new ApolloClient({ cache: new InMemoryCache(), link: new HttpLink({ uri: "[YOUR HASURA GRAPHQL ENDPOINT]", }), }); const App = () => (          ); render(, document.getElementById("root"));
import React from "react"; import { useQuery, gql } from "@apollo/client"; import { List, ListItem } from "./shared/List"; import { Badge } from "./shared/Badge"; const PLANET = gql` query Planet($id: uuid!) { planets_by_pk(id: $id) { id name cuisine reviews { id body } } } `; const Planet = ({ match: { params: { id }, }, }) => { const { loading, error, data } = useQuery(PLANET, { variables: { id }, }); if (loading) return 

Loading ...

; if (error) return

Error :(

; const { name, cuisine, reviews } = data.planets_by_pk; return (

{name} {cuisine}

{reviews.map((review) => ( {review.body} ))} ); }; export default Planet;
import React from "react"; import { useQuery, gql } from "@apollo/client"; import { Link } from "react-router-dom"; import { List, ListItemWithLink } from "./shared/List"; import { Badge } from "./shared/Badge"; const PLANETS = gql` { planets { id name cuisine } } `; const Planets = ({ newPlanets }) => { const { loading, error, data } = useQuery(PLANETS); const renderPlanets = (planets) => { return planets.map(({ id, name, cuisine }) => (   {name} {cuisine}   )); }; if (loading) return 

Loading ...

; if (error) return

Error :(

; return ; }; export default Planets;

S TEP 4: S وآخرون تصل اشتراكات

We install new libraries and set up Apollo Client to support subscriptions. Then, we change our reviews query to a subscription so it can show live updates.

> npm install @apollo/link-ws subscriptions-transport-ws
import React from "react"; import { render } from "react-dom"; import { ApolloProvider, ApolloClient, HttpLink, InMemoryCache, split, } from "@apollo/client"; import { getMainDefinition } from "@apollo/client/utilities"; import { WebSocketLink } from "@apollo/link-ws"; import { BrowserRouter, Switch, Route } from "react-router-dom"; import PlanetSearch from "./components/PlanetSearch"; import Planet from "./components/Planet"; import Logo from "./components/shared/Logo"; import "./index.css"; const GRAPHQL_ENDPOINT = "[YOUR HASURA GRAPHQL ENDPOINT]"; const httpLink = new HttpLink({ uri: `//${GRAPHQL_ENDPOINT}`, }); const wsLink = new WebSocketLink({ uri: `ws://${GRAPHQL_ENDPOINT}`, options: { reconnect: true, }, }); const splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === "OperationDefinition" && definition.operation === "subscription" ); }, wsLink, httpLink ); const client = new ApolloClient({ cache: new InMemoryCache(), link: splitLink, }); const App = () => (          ); render(, document.getElementById("root"));
import React from "react"; import { useSubscription, gql } from "@apollo/client"; import { List, ListItem } from "./shared/List"; import { Badge } from "./shared/Badge"; const PLANET = gql` subscription Planet($id: uuid!) { planets_by_pk(id: $id) { id name cuisine reviews { id body } } } `; const Planet = ({ match: { params: { id }, }, }) => { const { loading, error, data } = useSubscription(PLANET, { variables: { id }, }); if (loading) return 

Loading ...

; if (error) return

Error :(

; const { name, cuisine, reviews } = data.planets_by_pk; return (

{name} {cuisine}

{reviews.map((review) => ( {review.body} ))} ); }; export default Planet;
صفحة الكوكب مع الاستعراضات الحية

Step 5: Do a sandworm dance

We've implemented planets with live reviews! Do a little dance to celebrate before getting down to serious business.

رقصة الدودة

Part 3: Business logic

Step 1: Add input form

We want a way to submit reviews through our UI. We rename our search form to be a generic InputForm and add it above the review list.

import React, { useState } from "react"; import { useSubscription, gql } from "@apollo/client"; import { List, ListItem } from "./shared/List"; import { Badge } from "./shared/Badge"; import InputForm from "./shared/InputForm"; const PLANET = gql` subscription Planet($id: uuid!) { planets_by_pk(id: $id) { id name cuisine reviews(order_by: { created_at: desc }) { id body created_at } } } `; const Planet = ({ match: { params: { id }, }, }) => { const [inputVal, setInputVal] = useState(""); const { loading, error, data } = useSubscription(PLANET, { variables: { id }, }); if (loading) return 

Loading ...

; if (error) return

Error :(

; const { name, cuisine, reviews } = data.planets_by_pk; return (

{name} {cuisine}

setInputVal(e.target.value)} onSubmit={() => {}} buttonText="Submit" /> {reviews.map((review) => ( {review.body} ))} ); }; export default Planet;

Step 2: Test review mutation

We'll use a mutation to add new reviews. We test our mutation with GraphiQL in the Hasura console.

أدخل طفرة المراجعة في GraphiQL

And convert it to accept variables so we can use it in our code.

أدخل طفرة المراجعة مع المتغيرات

Step 3: Create action

The Bene Gesserit have requested us to not allow (cough censor cough) the word "fear" in the reviews. We create an action for the business logic that will check for this word whenever a user submits a review.

Inside our freshly minted action, we go to the "Codegen" tab.

We select the nodejs-express option, and copy the handler boilerplate code below.

كود Boilerplate لـ nodejs-express

We click "Try on Glitch," which takes us to a barebones express app, where we can paste our handler code.

لصق رمز المعالج الخاص بنا في Glitch

Back inside our action, we set our handler URL to the one from our Glitch app, with the correct route from our handler code.

معالج URL

We can now test our action in the console. It runs like a regular mutation, because we don't have any business logic checking for the word "fear" yet.

اختبار عملنا في وحدة التحكم

Step 4: Add business logic

In our handler, we add business logic that checks for "fear" inside the body of the review. If it's fearless, we run the mutation as usual. If not, we return an ominous error.

التحقق من منطق الأعمال لـ

If we run the action with "fear" now, we get the error in the response:

اختبار منطق عملنا في وحدة التحكم

Step 5: Order reviews

Our review order is currently topsy turvy. We add a created_at column to the reviews table so we can order by newest first.

reviews(order_by: { created_at: desc })

Step 6: Add review mutation

Finally, we update our action syntax with variables, and copy paste it into our code as a mutation. We update our code to run this mutation when a user submits a new review, so that our business logic can check it for compliance (ahem obedience ahem) before updating our database.

import React, { useState } from "react"; import { useSubscription, useMutation, gql } from "@apollo/client"; import { List, ListItem } from "./shared/List"; import { Badge } from "./shared/Badge"; import InputForm from "./shared/InputForm"; const PLANET = gql` subscription Planet($id: uuid!) { planets_by_pk(id: $id) { id name cuisine reviews(order_by: { created_at: desc }) { id body created_at } } } `; const ADD_REVIEW = gql` mutation($body: String!, $id: uuid!) { AddFearlessReview(body: $body, id: $id) { affected_rows } } `; const Planet = ({ match: { params: { id }, }, }) => { const [inputVal, setInputVal] = useState(""); const { loading, error, data } = useSubscription(PLANET, { variables: { id }, }); const [addReview] = useMutation(ADD_REVIEW); if (loading) return 

Loading ...

; if (error) return

Error :(

; const { name, cuisine, reviews } = data.planets_by_pk; return (

{name} {cuisine}

setInputVal(e.target.value)} onSubmit={() => { addReview({ variables: { id, body: inputVal } }) .then(() => setInputVal("")) .catch((e) => { setInputVal(e.message); }); }} buttonText="Submit" /> {reviews.map((review) => ( {review.body} ))} ); }; export default Planet;

If we submit a new review that includes "fear" now, we get our ominous error, which we display in the input field.

اختبار عملنا عبر واجهة المستخدم

Step 7: We did it! ?

Congrats on building a full-stack React & GraphQL app!

كفك

What does the future hold?

spice_must_flow.jpg

If only we had some spice melange, we would know. But we built so many features in so little time! We covered GraphQL queries, mutations, subscriptions, routing, searching, and even custom business logic with Hasura actions! I hope you had fun coding along.

ما الميزات الأخرى التي ترغب في رؤيتها في هذا التطبيق؟ تواصل معي على Twitter ، وسأقدم المزيد من البرامج التعليمية! إذا كنت مصدر إلهام لإضافة ميزات بنفسك ، فالرجاء المشاركة - أحب أن أسمع عنها :)