ãã£ããã¢ããªã®ãããªãã®ãä½ã£ã¦ã¿ããã¨æããReact ã§ç°¡æçãªWebãã£ããã¢ããªãä½æãã¦ã¿ã¾ããã
Â
ã½ã¼ã¹ã³ã¼ã ã®å
¨éã¯ä»¥ä¸ã§ãã
Â
Â
ä½æããã¢ããª
åç»ãã£ããã£â
Â
Â
ã·ã¹ãã æ§æ
以ä¸ã®æ©è½ãæã¤ã¢ããªãä½æãã¾ããã
ãã¼ã ç»é¢
ã¡ãã»ã¼ã¸ã®éä¿¡å
ã¨éä¿¡å
ã®ã¦ã¼ã¶ã¼ãããããé¸æ
ããã£ããã«ã¼ã ã¸ãã®ãªã³ã¯ãã¯ãªãã¯ãããã¨ã§ãã£ããã«ã¼ã ç»é¢ã«é·ç§»ãã
ãã£ããã«ã¼ã ç»é¢
ç»é¢ãï¼åå²ãããã¼ã ç»é¢ã§é¸æããéä¿¡å
ã¨éä¿¡å
ã®ã¦ã¼ã¶ã¼ããããã®ã¡ãã»ã¼ã¸ã¨ãªã¢ã表示
ããããã®ã¦ã¼ã¶ã¼ã¯ã¡ãã»ã¼ã¸ã®éä¿¡ãå¯è½
éä¿¡ããã¡ãã»ã¼ã¸ã¯èªèº«ã®ã¡ãã»ã¼ã¸ã¨ãªã¢ã«è¿½è¨ããã¦ãã
Â
主ãªä½¿ç¨æè¡
ããã³ãã¨ã³ã
Node.js (v16.20.2)
React (v18.2.0)
TypeScript (v4.9.5)
Apollo Client (v3.9.5)
tailwindcssÂ
ããã¯ã¨ã³ã
å®è£
ã«ã¤ãã¦
ããã¯ã¨ã³ã
ããã¯ã¨ã³ã㯠golang 㧠GraphQL ãµã¼ãã¼ãæ§ç¯ããã¡ãã»ã¼ã¸ã®éåä¿¡ãè¡ãããã«ãã¾ããã
ã¡ãã»ã¼ã¸ãæ稿ããããç¬æã«ç»é¢ã«åæ ããã«ã¯ã©ãããã°ãããèããæã«ãGraphQL ã® Subscription ã§ã¡ãã»ã¼ã¸ã®åä¿¡ãç£è¦ããã°ããã®ã§ã¯ãªããã¨æãã試ãã¦ã¿ã¾ããã
Â
ãªããgolang 㧠GraphQL ã®ä½æã¯ä¸è¨ã¨ã¦ã詳ããæ¸ãã¦ãã ãã£ã¦ãã¾ããã
ãã¡ãã大å¤åèã«ããã¦ããã ãã¾ããã
Â
www.ohitori.fun
Â
schema.graphqls
# GraphQL schema example # # https://gqlgen.com/getting-started/
type Message { Â id: ID! Â text: String! Â createdAt: String! Â userId: ID! }
type User { Â id: ID! Â name: String! Â createdAt: String! Â deletedAt: String! }
type Query { Â messages: [Message!]! }
input NewMessage { Â text: String! Â userId: ID! }
type Mutation { Â postMessage(input: NewMessage!): Message! }
type Subscription { Â messagePosted(userId: ID!): Message! }
Â
å種ã¹ãã¼ã ãå®ç¾©ãã¦ãã¾ãã
Message: ããåãããããã£ããã¡ãã»ã¼ã¸
User: ã¦ã¼ã¶ã¼æ
å ±
Mutation: ã¡ãã»ã¼ã¸ã®æ稿å¦çãNewMessage åã®ã¡ãã»ã¼ã¸å
容ãåãä»ããå¦çãå®è£
ãã¦ããã¾ãã
Subscription: ã¡ãã»ã¼ã¸ã®è³¼èªå¦çãæå®ã®ã¦ã¼ã¶ã¼IDã«ã¤ãã¦æ稿ãããã¡ãã»ã¼ã¸ãè³¼èªããå¦çãå®è£
ãã¦ããã¾ãã
â» Query ã¯ã¹ãã¼ã å®ç¾©ãã¦ãã¾ãããä»åã¯ä½¿ã£ã¦ãã¾ãã
schema.resolvers.go
package graph
// This file will be automatically regenerated based
// on the schema, any resolver implementations
// will be copied through when generating and any unknown code
// will be moved to the end.
// Code generated by github .com/99designs/gqlgen version v0.17.44
import (
  "backend/graph/model"
  "context"
  "fmt"
  "log"
  "time"
  "github .com/segmentio/ksuid"
)
// PostMessage is the resolver for the postMessage field.
func ( r * mutationResolver ) PostMessage ( ctx context . Context ,
input model . NewMessage ) ( * model . Message , error ) {
  message := & model . Message {
    ID :     ksuid . New (). String (),
    CreatedAt : time . Now (). Format ( time . RFC3339 ),
    UserID :   input . UserID ,
    Text :    input . Text ,
  }
  // æ稿ãããã¡ãã»ã¼ã¸ãä¿åããsubscribeãã¦ããå
¨ã¦ã®ã³ãã¯ã·ã§ã³ã«
//ããã¼ããã£ã¹ã
  r . mutex . Lock ()
  r.messages = append ( r . messages , message )
  for _ , ch := range r . subscribers {
    ch <- message
  }
  r . mutex . Unlock ()
}
// Messages is the resolver for the messages field.
func ( r * queryResolver ) Messages ( ctx context . Context ) (* model . Message , error ) {
  return r . messages , nil
}
// MessagePosted is the resolver for the messagePosted field.
func ( r * subscriptionResolver ) MessagePosted ( ctx context . Context , userID string )
( <- chan * model . Message , error ) {
  r . mutex . Lock ()
  defer r . mutex . Unlock ()
  // ãã§ã«ãµãã¹ã¯ã©ã¤ãããã¦ããããã§ãã¯
  if _ , ok := r . subscribers [ userID ]; ok {
    err := fmt . Errorf ( "`%s` has already been subscribed" , userID )
    log . Print ( err . Error ())
  }
  // ãã£ã³ãã«ãä½æãããªã¹ãã«ç»é²
  ch := make ( chan * model . Message , 1 )
  r . subscribers [ userID ] = ch
  log . Printf ( "`%s` has been subscribed!" , userID )
  // ã³ãã¯ã·ã§ã³ãçµäºãããããã®ãã£ã³ãã«ãåé¤ãã
  go func () {
    <- ctx . Done ()
    r . mutex . Lock ()
    defer r . mutex . Unlock ()
    delete ( r . subscribers , userID )
    close ( ch )
    log . Printf ( "`%s` has been unsubscribed." , userID )
  }()
  // ãã®ãã£ã³ãã«ãå©ç¨ããã¡ãã»ã¼ã¸ãé¸æããããã®ãã£ã«ã¿ãªã³ã°
  filteredCh := make ( chan * model . Message )
  go func () {
    for msg := range ch {
      // ã¡ãã»ã¼ã¸ã®ã¦ã¼ã¶ã¼IDã¨ãµãã¹ã¯ã©ã¤ãã¼ ã®ã¦ã¼ã¶ã¼IDãæ¯è¼
      if msg . UserID == userID {
        filteredCh <- msg
      }
    }
  }()
  return filteredCh , nil
}
// Mutation returns MutationResolver implementation.
func ( r * Resolver ) Mutation () MutationResolver { return & mutationResolver { r } }
// Query returns QueryResolver implementation.
func ( r * Resolver ) Query () QueryResolver { return & queryResolver { r } }
// Subscription returns SubscriptionResolver implementation.
func ( r * Resolver ) Subscription () SubscriptionResolver {
return & subscriptionResolver { r } }
type mutationResolver struct { * Resolver }
type queryResolver struct { * Resolver }
type subscriptionResolver struct { * Resolver }
Â
(Mutation) PostMessage: ã¡ãã»ã¼ã¸å
容ãåãåããåãåã£ãã¡ãã»ã¼ã¸ã Subscriber ã«éä¿¡ãMutex ã使ãæä»å¶å¾¡ ãè¡ã£ã¦ãã¾ã
(Subscription) MessagePosted: ã¡ãã»ã¼ã¸åä¿¡ç¨ã®ãã£ãã«ãçæããuserID ããã¼ã«è³¼èªãã£ãã«ãªã¹ãã«è¿½å ãåä¿¡ãããã¡ãã»ã¼ã¸ãè³¼èªå¯¾è±¡ã® userID ã¨ä¸è´ããå ´åã«ã¡ãã»ã¼ã¸ãåä¿¡ããã¾ã
GraphQL ãµã¼ãã¼
server.go
package main
import (
  "backend/graph"
  "log"
  "net/http"
  "os"
  "time"
  "github .com/99designs/gqlgen/graphql/handler"
  "github .com/99designs/gqlgen/graphql/handler/transport"
  "github .com/99designs/gqlgen/graphql/playground"
  "github .com/gorilla/websocket"
)
const defaultPort = "8080"
func main () {
  port := os . Getenv ( "PORT" )
  if port == "" {
    port = defaultPort
  }
    graph . NewExecutableSchema (
      graph . Config { Resolvers : graph . NewResolver ()}),
  )
  // add ws transport configured by ourselves
  srv . AddTransport ( transport . Options {})
  srv . AddTransport ( transport . GET {})
  srv . AddTransport ( transport . POST {})
  srv . AddTransport ( transport . MultipartForm {})
  srv . AddTransport ( & transport . Websocket {
    Upgrader : websocket . Upgrader {
      //ReadBufferSize:  1024,
      //WriteBufferSize: 1024,
      CheckOrigin : func ( r * http . Request ) bool {
        // add checking origin logic to decide return true or false
        return true
      },
    },
    KeepAlivePingInterval : 10 * time . Second ,
  })
  http . Handle ( "/" , playground . Handler ( "GraphQL playground" , "/query" ))
  http . Handle ( "/query" , cors ( srv . ServeHTTP ))
  log . Fatal ( http . ListenAndServe ( ":" + port , nil ))
}
func cors ( h http . HandlerFunc ) http . HandlerFunc {
  return func ( w http . ResponseWriter , r * http . Request ) {
    w . Header (). Set ( "Access -Control-Allow-Origin" , "*" )
    w . Header (). Set ( "Access -Control-Allow-Methods" , "*" )
    w . Header (). Set ( "Access -Control-Allow-Headers" , "*" )
    h ( w , r )
  }
}
Â
ãhttp://localhost:8080/query ãã®ã¨ã³ããã¤ã³ã㧠GraphQL ãµã¼ãã¼ãç«ã¡ä¸ãã
ãsrv .AddTransport(&transport.Websocketãã®ç®æ㧠WebSocket éä¿¡ãã§ããããã«å®ç¾©ãã¦ãã¾ã (ããã³ãå´ãã Subscription ã§ããããã«ãã)
Â
ããã³ãã¨ã³ã
ããã³ãã¨ã³ã㯠React + TypeScript ã§ä½æãã¦ãã¾ãã
å人çã«ç»é¢ä½ãã¨ã㯠React ã使ããã¨ãå¤ãã§ãã
Â
ä»åã¯ããã¯ã¨ã³ãã§ä½æãã GraphQL ãµã¼ãã¼ã«å¯¾ãã¦ã
React å´ã GraphQL ã¯ã©ã¤ã¢ã³ãã«ãªãããã§ããã
Apollo Client ãæåãªãããªã®ã§ä½¿ã£ã¦ã¿ã¾ããã
å
¬å¼ããã¥ã¡ã³ããå
å®ãã¦ãã¦ããããããã£ãã§ãã
Â
www.apollographql.com
Â
GraphQL ã¯ã©ã¤ã¢ã³ãå®ç¾©
const httpLink = new HttpLink ({
});
const wsLink = new GraphQLWsLink (
 createClient ({
 })
);
const splitLink = split (
 ({ query }) => {
  const definition = getMainDefinition ( query );
  return (
   definition . kind === "OperationDefinition" &&
   definition . operation === "subscription"
  );
 },
 wsLink ,
 httpLink
);
const client = new ApolloClient ({
 link : splitLink ,
 cache : new InMemoryCache (),
});
const root = ReactDOM . createRoot (
 document . getElementById ( "root" ) as HTMLElement
);
root . render (
 < ApolloProvider client = { client } >
  < App />
 </ ApolloProvider >
);
Â
å
¬å¼ããã¥ã¡ã³ãã®ä¾ã«å¾ããApolloClient ãå®ç¾©
ApolloClientProvider ã§ã©ãããããã¨ã§ãApp ã³ã³ãã¼ãã³ã 以ä¸ã§ useQuery ãªã© GraphQL ããã¯ã使ããããã«ãªãããã§ãã
httpLink 㯠Query ã Mutation ã®ã¨ã³ããã¤ã³ãã¨ãã¦ãwsLink 㯠Subscription ã®ã¨ã³ããã¤ã³ãã¨ãã¦ä½¿ããã¾ãã
ã¡ãã»ã¼ã¸æ稿Â
mutation.ts
import { gql } from "@apollo /client" ;
export const POST_MESSAGE_MUTATION = gql `
 mutation ($input: NewMessage!) {
  postMessage(input: $input) {
   id
   userId
   text
   createdAt
  }
 }
` ;
ChatTextField.tsx
import { useMutation } from "@apollo /client" ;
import { useState } from "react" ;
import { POST_MESSAGE_MUTATION } from "../../apis/mutation" ;
function ChatTextField ({ targetUserID } : { targetUserID : string }) {
 const [ message , setMessage ] = useState ( "" );
 const [ postMessage , { data , loading , error }] = useMutation (
  POST_MESSAGE_MUTATION
 );
 return (
  < div className = "flex p-3" >
   < input
    type = "text"
    onChange = { ( e ) => setMessage ( e . target . value ) }
    className = "flex -auto mr-2 rounded-l p-3"
   />
   < button
    onClick = { () => {
     postMessage ({
      variables : { input : { text : message , userId : targetUserID } },
     });
    } }
    className = "rounded-r p-3 bg-blue-500 text-white
     transition duration-300 hover:bg-blue-600"
   >
    éä¿¡
   </ button >
  </ div >
 );
}
export default ChatTextField ;
Â
gql ã§ã©ããã㦠Query ã Mutation, Subscription ãå®ç¾©ã§ãã¾ãã
useMutation ããã¯ã«ä¸è¨ã® Mutation å®ç¾©ã渡ããã¨ã§ãMutation ãå®è¡ããé¢æ°ãããã³ãã®çµæå¤æ°ãå¾ããã¾ãã
useMutation ããã¯ãå®ç¾©ããã ãã§ã¯å®è¡ã¯ãããã以ä¸ã®ããã« onClick ãããªã¬ã¼ã«é¢æ°å¼ã³åºãã®ããã«å¼ã³åºããã¨ã§å®è¡ãããã¨ãã§ãã¾ãã
< button
    onClick = { () => {
     postMessage ({
      variables : { input : { text : message , userId : targetUserID } },
     });
    } }
Â
ã¡ãã»ã¼ã¸ãªã¢ã«ã¿ã¤ã åä¿¡
subscription.ts
import { gql } from "@apollo /client" ;
export const MESSAGE_POSTED_SUBSCRIPTION = gql `
 subscription ($userID: ID!) {
  messagePosted(userId: $userID) {
   id
   userId
   text
   createdAt
  }
 }
` ;
ChatMessage.tsx
||<
import { useSubscription } from "@apollo /client" ;
import { MESSAGE_POSTED_SUBSCRIPTION } from "../../apis/subscription" ;
import { formatDate } from "../../util/date_util" ;
import { useEffect , useState } from "react" ;
import { userIconMap } from "../../constant/Constant" ;
function ChatMessage ({
 userID ,
 className ,
} : {
 userID : string ;
 className ?: string ;
}) {
 const [ messages , setMessages ] = useState < any >([]);
 const { data , loading } = useSubscription ( MESSAGE_POSTED_SUBSCRIPTION , {
  variables : { userID },
 });
 useEffect ( () => {
  if ( data && data . messagePosted ) {
   setMessages ( ( prevMessages ) => [ ... prevMessages , data . messagePosted ]);
  }
 }, [ data ]);
 return (
  < div className = { ` ${ className } p-3` } >
   { ! loading && (
    <>
     { messages . map ( ( data ) => (
      <>
       < p className = "text-xs p-3" >
        { formatDate ( data . createdAt , "MMæddæ¥(E) HH:mm" ) }
       </ p >
       < div className = "flex h-8" >
        < div className = "mr-6" > { userIconMap [ userID ] || null } </ div >
        < p className = "flex -auto h-8 bg-blue-500 text-white
         rounded-lg p-3 font-bold flex items-center" >
         { data . text }
        </ p >
       </ div >
      </>
     )) }
    </>
   ) }
  </ div >
 );
}
export default ChatMessage ;
Â
Â
Â
useSubscription ããã¯ã« Subscription å®ç¾©ã渡ããã¨ã§ã³ã³ãã¼ãã³ã ã¬ã³ããªã³ã° æã«è³¼èªãéå§ããã¾ãã
Mutation ãå®è¡ããã¦ããã¼ããã£ã¹ããããã¨ãããã§è³¼èªãã¦ãã data å¤æ°ã«å¤ãåä¿¡ãããã³ã³ãã¼ãã³ã ãåæç»ããã¾ãã
ä¸è¨ã®ä¾ã§ã¯ãç¶æ
管çãã¦ãã messages å¤æ°ã«åä¿¡ãããã¡ãã»ã¼ã¸ã追å ãã¦ãã¾ãã
Â
以ä¸ã«ãªãã¾ãã
Apollo Client ã使ããã¨ã§ã·ã³ãã«ã« GraphQL ã¯ã©ã¤ã¢ã³ããæ§ç¯ã§ããã®ãè¯ãçºè¦ã«ãªãã¾ããã