Cách tạo bản sao Yelp đầy đủ với React & GraphQL (Dune World Edition)

Tôi không được sợ hãi. Sợ hãi thường giết chết tư duy. Sợ hãi là cái chết nhỏ mang đến sự xóa sổ hoàn toàn. Tôi sẽ phải đối mặt với nỗi sợ hãi của tôi. Tôi sẽ cho phép nó vượt qua tôi và qua tôi. Và khi nó đã đi qua tôi sẽ hướng con mắt bên trong để nhìn thấy con đường của nó. Trường hợp sự sợ hãi đã biến mất sẽ không có gì. Sẽ chỉ có tôi.

- "Litany chống lại nỗi sợ hãi," Frank Herbert, Dune

Bạn có thể tự hỏi, "Nỗi sợ hãi liên quan gì đến ứng dụng React?" Trước hết, không có gì phải sợ trong một ứng dụng React. Trên thực tế, trong ứng dụng cụ thể này, chúng tôi đã cấm nỗi sợ hãi. Thật tuyệt phải không?

Bây giờ bạn đã sẵn sàng để không sợ hãi, hãy thảo luận về ứng dụng của chúng tôi. Đó là một bản sao Yelp mini, nơi thay vì xem xét các nhà hàng, người dùng xem xét các hành tinh trong loạt phim khoa học viễn tưởng cổ điển, Dune. (Tại sao? Bởi vì có một bộ phim Dune mới sắp ra mắt ... nhưng hãy quay lại vấn đề chính.)

Để xây dựng ứng dụng đầy đủ, chúng tôi sẽ sử dụng các công nghệ giúp cuộc sống của chúng tôi trở nên dễ dàng.

  1. React: Khuôn khổ giao diện người dùng trực quan, tổng hợp, bởi vì bộ não của chúng ta thích sáng tác mọi thứ.
  2. GraphQL: Bạn có thể đã nghe nhiều lý do tại sao GraphQL lại tuyệt vời. Cho đến nay, điều quan trọng nhất là năng suất và hạnh phúc của nhà phát triển .
  3. Hasura: Thiết lập API GraphQL được tạo tự động trên cơ sở dữ liệu Postgres trong vòng chưa đầy 30 giây.
  4. Heroku: Để lưu trữ cơ sở dữ liệu của chúng tôi.

Và GraphQL mang lại cho tôi hạnh phúc như thế nào?

Tôi thấy bạn là một người đa nghi. Nhưng rất có thể bạn sẽ đến ngay sau khi bạn dành một chút thời gian với GraphiQL (sân chơi GraphQL).

Sử dụng GraphQL là một điều dễ dàng cho nhà phát triển front-end, so với các cách cũ của các điểm cuối REST rườm rà. GraphQL cung cấp cho bạn một điểm cuối duy nhất lắng nghe mọi rắc rối của bạn ... Ý tôi là các truy vấn. Đó là một người lắng nghe tuyệt vời đến mức bạn có thể nói chính xác những gì bạn muốn, và nó sẽ đưa nó cho bạn, không hơn không kém.

Cảm thấy rung động về trải nghiệm trị liệu này? Hãy đi sâu vào hướng dẫn để bạn có thể thử nó càng sớm càng tốt!

?? Đây là repo nếu bạn muốn viết mã.

P art 1: S earch

S tep 1: D eploy to Heroku

Bước đầu tiên của mọi hành trình tốt đẹp là ngồi xuống với một ít trà nóng và nhấm nháp một cách bình tĩnh. Sau khi làm xong, chúng tôi có thể triển khai Heroku từ trang web Hasura. Điều này sẽ thiết lập cho chúng tôi mọi thứ chúng tôi cần: cơ sở dữ liệu Postgres, công cụ Hasura GraphQL của chúng tôi và một số đồ ăn nhẹ cho hành trình.

sách đen.png

Bước 2: Tạo bảng hành tinh

Người dùng của chúng tôi muốn xem xét các hành tinh. Vì vậy, chúng tôi tạo một bảng Postgres thông qua bảng điều khiển Hasura để lưu trữ dữ liệu hành tinh của chúng tôi. Đáng chú ý là hành tinh ma quỷ, Giedi Prime, đã thu hút sự chú ý với những món ăn độc đáo.

Bảng hành tinh

Trong khi đó trong tab GraphiQL: Hasura đã tự động tạo lược đồ GraphQL của chúng tôi! Chơi xung quanh với Explorer ở đây ??

GraphiQL Explorer

S tep 3: C reate ứng dụng React

Chúng tôi sẽ cần một giao diện người dùng cho ứng dụng của mình, vì vậy chúng tôi tạo một ứng dụng React và cài đặt một số thư viện cho các yêu cầu, định tuyến và kiểu GraphQL. (Đảm bảo rằng bạn đã cài đặt Node trước.)

> 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 et up Apollo Client

Apollo Client sẽ giúp chúng tôi với các yêu cầu mạng GraphQL và bộ nhớ đệm, vì vậy chúng tôi có thể tránh tất cả những công việc khó khăn đó. Chúng tôi cũng thực hiện truy vấn đầu tiên và liệt kê các hành tinh của chúng tôi! Ứng dụng của chúng tôi đang bắt đầu hình thành.

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"));

Chúng tôi kiểm tra truy vấn GraphQL của mình trong bảng điều khiển Hasura trước khi sao chép-dán nó vào mã của chúng tôi.

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 tep 5: S tyle list

Danh sách hành tinh của chúng ta là tốt đẹp và tất cả, nhưng nó cần một chút thay đổi với Cảm xúc (xem repo để biết đầy đủ các kiểu).

Danh sách các hành tinh được tạo kiểu

S tep 6: S earch form & state

Người dùng của chúng tôi muốn tìm kiếm các hành tinh và sắp xếp chúng theo tên. Vì vậy, chúng tôi thêm một biểu mẫu tìm kiếm truy vấn điểm cuối của chúng tôi bằng một chuỗi tìm kiếm và chuyển kết quả vào Planetsđể cập nhật danh sách hành tinh của chúng tôi. Chúng tôi cũng sử dụng React Hooks để quản lý trạng thái ứng dụng của mình.

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 tự hào

Chúng tôi đã triển khai danh sách hành tinh và các tính năng tìm kiếm! Chúng tôi âu yếm ngắm nhìn tác phẩm thủ công của mình, chụp một vài bức ảnh tự sướng cùng nhau và chuyển sang phần đánh giá.

Danh sách hành tinh có tìm kiếm

P art 2: L ive đánh giá

Bảng đánh giá S tep 1: C reate

Người dùng của chúng tôi sẽ đến thăm những hành tinh này và viết đánh giá về trải nghiệm của họ. Chúng tôi tạo một bảng thông qua bảng điều khiển Hasura cho dữ liệu đánh giá của chúng tôi.

Bảng đánh giá

We add a foreign key from the planet_id column to the id column in the planets table, to indicate that planet_ids of reviews have to match id's of planets.

Khóa ngoại

Step 2: Track relationships

Each planet has multiple reviews, while each review has one planet: a one-to-many relationship. We create and track this relationship via the Hasura console, so it can be exposed in our GraphQL schema.

Theo dõi các mối quan hệ

Now we can query reviews for each planet in the Explorer!

Truy vấn đánh giá hành tinh

Step 3: Set up routing

We want to be able to click on a planet and view its reviews on a separate page. We set up routing with React Router, and list reviews on the planet page.

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;

Step 4: Set up subscriptions

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;
Trang Planet với các bài đánh giá trực tiếp

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.

Vũ điệu giun

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.

Chèn đột biến đánh giá trong GraphiQL

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

Chèn đánh giá đột biến với các biến

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.

Mã soạn sẵn cho nodejs-express

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

Dán mã trình xử lý của chúng tôi trong 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 của trình xử lý

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.

Kiểm tra hành động của chúng tôi trong bảng điều khiển

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.

Kiểm tra logic nghiệp vụ cho

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

Kiểm tra logic nghiệp vụ của chúng tôi trong bảng điều khiển

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.

Kiểm tra hành động của chúng tôi qua giao diện người dùng

Step 7: We did it! ?

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

Đập tay

What does the future hold?

Cay_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.

Những tính năng nào khác mà bạn muốn thấy trong ứng dụng này? Liên hệ với tôi trên Twitter và tôi sẽ thực hiện nhiều hướng dẫn hơn! Nếu bạn có cảm hứng để tự thêm các tính năng, hãy chia sẻ - tôi rất muốn nghe về chúng :)