用Next.js和Go语言(gqlgen)构建使用GraphQL的应用程序的方法

在这篇文章中,我们将介绍使用Next.js作为前端和Go语言(gqlgen)作为后端,使用GraphQL构建前后端API的应用程序的方法。

image.png

背景

在我个人看来,我一直觉得使用前端框架和Go语言进行开发会很有意思。
在这种情况下,我听说React框架的Next.js使用起来很方便。

我同时还了解到Next.js的示例非常丰富,并且可以轻松创建使用Apollo的应用程序模板,所以我决定将其与Go语言的后端结合起来尝试运行。

构成要素

Untitled_LINE_Beacon_-_Cacoo.png
名前種別役割JavaScriptプログラミング言語今回のフロントエンドの実装に用いるプログラミング言語ReactライブラリコンポーネントベースでUIを構築できるJavaScriptライブラリApollo ClientライブラリGraphQLに対応した状態管理ライブラリNext.jsフレームワークReactのサーバーサイドレンダリング(SSR)に対応するフレームワークGraphQLクエリ言語/ランタイムAPI向けに作られたクエリ言語およびランタイムGo言語プログラミング言語今回のバックエンドの実装に用いるプログラミング言語, golangと表記されることもあるgqlgenライブラリSchemaベースでGraphQLサーバを構築するためのライブラリ

UI界面的构建

首先,我们将基于Next.js的examples/with-apollo示例来创建应用程序。

$ yarn create next-app
success Installed "create-next-app@9.4.4" with binaries:
      - create-next-app
✔ What is your project named? … with-apollo-ui
✔ Pick a template › Example from the Next.js repo
✔ Pick an example › with-apollo
Creating a new Next.js app in /Users/yokazaki/src/github.com/yuuu/with-apollo-ui.

# ログ省略

$ cd with-apollo-ui
$ yarn dev

这个开发出来的应用程序可以一起注册和浏览URL和标题,它是一种简易书签应用程序。

image.png

在此时,向后端的GraphQL服务器发送请求时,使用的是公开在互联网上的服务器。因此,所列出的URL和标题是全球用户注册的内容,会直接显示出来。

建立GraphQL服务器

考虑到能够基于Schema构建GraphQL服务器以及功能的可扩展性,我们参考了以下文章,并使用Echo+gqlgen进行了构建。

使用 gqlgen + Echo 在 golang 上创建一个 GraphQL 服务器的教程。

基础建设

$ mkdir with-apollo-api
$ cd with-apollo-api
$ go mod init github.com/yuuu/with-apollo-api
$ go get github.com/99designs/gqlgen
$ go get github.com/rs/cors

# gqlgenでgraph/schema.graphqlsを生成
$ gqlgen init

模式定义

将GraphQL模式写入graph/schema.graphqls文件中。

type Post {
  id: ID!
  title: String!
  votes: Int!
  url: String!
  createdAt: String!
}

type PostsMeta {
  count: Int!
}

type Query {
  allPosts(orderBy: OrderBy, first: Int!, skip: Int!): [Post!]!
  _allPostsMeta: PostsMeta!
}

enum OrderBy {
  createdAt_ASC,
  createdAt_DESC
}

type Mutation {
  createPost(title: String!, url: String!): Post!
  updatePost(id: ID!, votes: Int): Post!
}

写完模式后,将生成源代码。

$ rm graph/schema.resolvers.go
$ gqlgen

实现查询和变更。

将生成的graph/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.

import (
    "context"
    "fmt"
    "sort"
    "strconv"
    "time"

    "github.com/yuuu/with-apollo-api/graph/generated"
    "github.com/yuuu/with-apollo-api/graph/model"
)

var posts []*model.Post = make([]*model.Post, 0)

func (r *mutationResolver) CreatePost(ctx context.Context, title string, url string) (*model.Post, error) {
    post := model.Post{
        ID:        fmt.Sprintf("%d", len(posts)+1),
        Title:     title,
        URL:       url,
        Votes:     0,
        CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
    }
    posts = append(posts, &post)
    return &post, nil
}

func (r *mutationResolver) UpdatePost(ctx context.Context, id string, votes *int) (*model.Post, error) {
    if votes == nil {
        return nil, nil
    }
    i, _ := strconv.Atoi(id)
    posts[i-1].Votes = *votes
    return posts[i-1], nil
}

func (r *queryResolver) AllPosts(ctx context.Context, orderBy *model.OrderBy, first int, skip int) ([]*model.Post, error) {
    if skip > len(posts) {
        skip = len(posts)
    }
    if (skip + first) > len(posts) {
        first = len(posts) - skip
    }
    sortedPosts := make([]*model.Post, len(posts))
    copy(sortedPosts, posts)
    if orderBy != nil && *orderBy == "createdAt_DESC" {
        sort.SliceStable(sortedPosts, func(i, j int) bool {
            return sortedPosts[i].CreatedAt > sortedPosts[j].CreatedAt
        })
    }
    slicePosts := sortedPosts[skip : skip+first]
    return slicePosts, nil
}

func (r *queryResolver) AllPostsMeta(ctx context.Context) (*model.PostsMeta, error) {
    postsMeta := model.PostsMeta{Count: len(posts)}
    return &postsMeta, nil
}

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

在操场上进行操作验证

使用以下命令启动GraphQL服务器。

$ go run server.go

当您在浏览器中访问 http://localhost:8080 ,将显示PlayGround。

image.png

将UI和GraphQL服务器结合在一起。

跨域資源共享設定

由于现状下,Next.js运行的源(http://localhost:3000)和GraphQL服务器的源(http://localhost:8080)不同,因此当前情况下向GraphQL服务器发送请求将会失败。

通过更改server.go文件,使其能够接受来自http://localhost:3000的请求。

package main

import (
    "log"
    "net/http"
    "os"

    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/99designs/gqlgen/graphql/playground"
    "github.com/rs/cors"
    "github.com/yuuu/with-apollo-api/graph"
    "github.com/yuuu/with-apollo-api/graph/generated"
)

const defaultPort = "8080"

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = defaultPort
    }

    srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))

    http.Handle("/", playground.Handler("GraphQL playground", "/query"))

    c := cors.New(cors.Options{
        AllowedOrigins:   []string{"http://localhost:3000", "http://localhost:8080"},
        AllowCredentials: true,
    })

    http.Handle("/query", c.Handler(srv))

    log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

更改Next.js的请求URL目标

在lib/apolloClient.js中将服务器URL更改为http://localhost:8080/query。

import { useMemo } from 'react'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'

let apolloClient

function createApolloClient() {
  return new ApolloClient({
    ssrMode: typeof window === 'undefined',
    link: new HttpLink({
      uri: 'http://localhost:8080/query', // Server URL (must be absolute)
      credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
    }),
    cache: new InMemoryCache(),
  })
}

export function initializeApollo(initialState = null) {
  const _apolloClient = apolloClient ?? createApolloClient()

  // If your page has Next.js data fetching methods that use Apollo Client, the initial state
  // gets hydrated here
  if (initialState) {
    _apolloClient.cache.restore(initialState)
  }
  // For SSG and SSR always create a new Apollo Client
  if (typeof window === 'undefined') return _apolloClient
  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient

  return _apolloClient
}

export function useApollo(initialState) {
  const store = useMemo(() => initializeApollo(initialState), [initialState])
  return store
}

确认操作

我們將同時啟動UI和GraphQL伺服器。

$ cd with-apollo-ui # 移動先パスは適宜変更ください
$ yarn dev

# 以下は別のterminalで
$ cd with-apollo-api # 移動先パスは適宜変更ください
$ go run server.go
image.png

总结

由于Next.js的样例非常丰富,我能够轻松地构建应用程序。如果我在其中添加认证和验证,并根据个人喜好自定义UI,那么很容易就可以发布服务。

我希望能够看到更多关于Next.js和Go语言的应用案例增加。大家也请务必尝试一下。

广告
将在 10 秒后关闭
bannerAds