用Next.js和Go语言(gqlgen)构建使用GraphQL的应用程序的方法
在这篇文章中,我们将介绍使用Next.js作为前端和Go语言(gqlgen)作为后端,使用GraphQL构建前后端API的应用程序的方法。
背景
在我个人看来,我一直觉得使用前端框架和Go语言进行开发会很有意思。
在这种情况下,我听说React框架的Next.js使用起来很方便。
我同时还了解到Next.js的示例非常丰富,并且可以轻松创建使用Apollo的应用程序模板,所以我决定将其与Go语言的后端结合起来尝试运行。
构成要素
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和标题,它是一种简易书签应用程序。
在此时,向后端的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。
将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
总结
由于Next.js的样例非常丰富,我能够轻松地构建应用程序。如果我在其中添加认证和验证,并根据个人喜好自定义UI,那么很容易就可以发布服务。
我希望能够看到更多关于Next.js和Go语言的应用案例增加。大家也请务必尝试一下。