使用”Nuxt.js/Apollo”作为前端,Go语言加上”gqlgen”作为后端,来创建GraphQL服务

请用中文进行以下的释义,只需要一个选项:

お題 → 话题

我們將通過一個簡單的ToDo應用程式(儘管在這篇文章中只能進行“新ToDo註冊”和“顯示所有已登記的ToDo”)作為主題,來確認使用GraphQL通信邏輯的效果。請注意,本次我們將不使用關係資料庫進行持久化,而是由後端返回固定值。

前提条件

对于Nuxt.js和Go,有个人或公司的开发经验,前端->后端的连接是通过REST(例如使用Axios)来实现的,但是希望尝试使用GraphQL的人可以参考。
然而,不需要写关于”GraphQL到底是什么?”或”与REST相比的优缺点是什么?”这些内容(因为已经有很多文章了)。

此外,根据题目要求,以最简结构进行实现。不考虑流行的Clean Architecture和DDD所遵循的包结构等。这方面,我之前写过一篇类似的文章。不过并不仅仅只有这篇文章,如果搜索一下,会有很多更好的文章出现。使用Golang实现的Clean Architecture(使用Echo、Gorm和wire)。

相关文章索引 (Guanlian Wenzhang Suoyin)

    • 第12回「GraphQLにおけるRelayスタイルによるページング実装再考(Window関数使用版)」

 

    • 第11回「Dataloadersを使ったN+1問題への対応」

 

    • 第10回「GraphQL(gqlgen)エラーハンドリング」

 

    • 第9回「GraphQLにおける認証認可事例(Auth0 RBAC仕立て)」

 

    • 第8回「GraphQL/Nuxt.js(TypeScript/Vuetify/Apollo)/Golang(gqlgen)/Google Cloud Storageの組み合わせで動画ファイルアップロード実装例」

 

    • 第7回「GraphQLにおけるRelayスタイルによるページング実装(後編:フロントエンド)」

 

    • 第6回「GraphQLにおけるRelayスタイルによるページング実装(前編:バックエンド)」

 

    • 第5回「DB接続付きGraphQLサーバ(by Golang)をローカルマシン上でDockerコンテナ起動」

 

    • 第4回「graphql-codegenでフロントエンドをGraphQLスキーマファースト」

 

    • 第3回「go+gqlgenでGraphQLサーバを作る(GORM使ってDB接続)」

 

    • 第2回「NuxtJS(with Apollo)のTypeScript対応」

 

    第1回「frontendに「nuxtjs/apollo」、backendに「go+gqlgen」の組み合わせでGraphQLサービスを作る」

开发环境

操作系统 – Linux(Ubuntu)

$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"

前端技术
前端开发
前端设计

Nuxt.js:助力于构建 Vue.js 应用程序的框架。

$ cat yarn.lock | grep "@nuxt/vue-app"
    "@nuxt/vue-app" "2.10.2"

包管理器 – Yarn

$ yarn -v
1.19.1

IDE – WebStorm

集成开发环境 – WebStorm

WebStorm 2019.2.4
Build #WS-192.7142.35, built on October 29, 2019

# 后端

说话 – 进行

$ go version
go version go1.13.3 linux/amd64

包管理器-Go模块

IDE – Goland
集成开发环境 – Goland

GoLand 2019.2.5
Build #GO-192.7142.48, built on November 8, 2019

请引用

GraphQL (用中文: 图灵数据查询语言)

前端技术

后端

实践

在GitHub上创建一个适当的代码库并进行本地git克隆操作。
这是根据我的环境所做的设置。

$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql

前端

使用 create-nuxt-app 命令创建一个用于前端的 Nuxt.js 项目。

工具的选择请参考以下参考。

    • UIはVuetify.js

SSRとSPAはどっちでもいいけどデフォのSSR
今回はテストまで考えてないのでテストフレームワークは無し
サーバーサイドとの接続はGraphQLなのでAxiosなどは無し

$ yarn create nuxt-app frontend
yarn create v1.19.1
   ・・・
success Installed "create-nuxt-app@2.11.1" with binaries:
      - create-nuxt-app

create-nuxt-app v2.11.1
✨  Generating Nuxt.js project in frontend
? Project name frontend
? Project description My ace Nuxt.js project
? Author name sky0621
? Choose the package manager Yarn
? Choose UI framework Vuetify.js
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose linting tools ESLint, Prettier
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
yarn run v1.19.1
$ eslint --ext .js,.vue --ignore-path .gitignore . --fix
Done in 2.90s.

?  Successfully created project frontend

目前的目录结构如下。

$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql
$
$ tree -L 2
.
├── frontend
│   ├── assets
│   ├── components
│   ├── layouts
│   ├── middleware
│   ├── node_modules
│   ├── nuxt.config.js
│   ├── package.json
│   ├── pages
│   ├── plugins
│   ├── README.md
│   ├── static
│   ├── store
│   └── yarn.lock
└── README.md

并且,为了保险起见,也把软件包的devDependencies列入。

$ cat frontend/package.json 
{
  ・・・
  "dependencies": {
    "nuxt": "^2.0.0"
  },
  "devDependencies": {
    "@nuxtjs/vuetify": "^1.0.0",
    "@nuxtjs/eslint-config": "^1.0.1",
    "@nuxtjs/eslint-module": "^1.0.0",
    "babel-eslint": "^10.0.1",
    "eslint": "^6.1.0",
    "eslint-plugin-nuxt": ">=0.4.2",
    "eslint-config-prettier": "^4.1.0",
    "eslint-plugin-prettier": "^3.0.1",
    "prettier": "^1.16.4"
  }
}

nuxtjs/apollo的安装引入

增加软件包

$ cd frontend/
$
$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql/frontend
$
$ yarn add @nuxtjs/apollo
yarn add v1.19.1
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.1.2: The platform "linux" is incompatible with this module.
  ・・・
Done in 15.54s.

向nuxt-config.js文件添加配置信息

$ git diff frontend/nuxt.config.js
diff --git a/frontend/nuxt.config.js b/frontend/nuxt.config.js
index 25a0fe4..afc0db4 100644
--- a/frontend/nuxt.config.js
+++ b/frontend/nuxt.config.js
@@ -42,7 +42,7 @@ export default {
   /*
    ** Nuxt.js modules
    */
-  modules: [],
+  modules: ['@nuxtjs/apollo'],
   /*
    ** vuetify module configuration
    ** https://github.com/nuxt-community/vuetify-module
@@ -64,6 +64,18 @@ export default {
       }
     }
   },
+
+  apollo: {
+    clientConfigs: {
+      default: {
+        // Goサーバを 8080 ポートで起動する予定のため
+        httpEndpoint: 'http://localhost:8080/'
+      }
+    },
+    // 任意だけど、これがないとGraphQL的なエラー起きた時に原因が掴みづらいため
+    errorHandler: '~/plugins/apollo-error-handler.js'
+  },
+
   /*
    ** Build configuration
    */

GraphQL错误处理插件

$ cat frontend/plugins/apollo-error-handler.js 
export default (error, context) => {
  console.log(error)
  context.error({ statusCode: 304, message: 'Server error' })
}

参考以下链接的内容:https://github.com/nuxt-community/apollo-module

前端启动

先试着启动一下,看看目前为止能达到什么程度。

$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql/frontend
$
$ yarn run dev
yarn run v1.19.1
$ nuxt

 WARN  Address localhost:8080 is already in use.                                                                                                                                                                                                           11:40:12

ℹ Trying a random port...                                                                                                                                                                                                                                  11:40:12

   ╭─────────────────────────────────────────────╮
   │                                             │
   │   Nuxt.js v2.10.2                           │
   │   Running in development mode (universal)   │
   │                                             │
   │   Listening on: http://localhost:44171/     │
   │                                             │
   ╰─────────────────────────────────────────────╯

ℹ Preparing project for development                                                                                                                                                                                                                        11:40:13
ℹ Initial build may take a while                                                                                                                                                                                                                           11:40:13
✔ Builder initialized                                                                                                                                                                                                                                      11:40:13
✔ Nuxt files generated                                                                                                                                                                                                                                     11:40:13

● Client █████████████████████████ building (35%) 215/225 modules 10 active
 node_modules/vuetify/dist/vuetify.js

● Server █████████████████████████ building (19%) 81/81 modules 0 active



 ERROR  Failed to compile with 1 errors                                                                                                                                                                                                    friendly-errors 11:40:31


 ERROR  in ./plugins/apollo-error-handler.js                                                                                                                                                                                               friendly-errors 11:40:31

Module Error (from ./node_modules/eslint-loader/dist/cjs.js):                                                                                                                                                                              friendly-errors 11:40:31

/home/sky0621/src/github.com/sky0621/study-graphql/frontend/plugins/apollo-error-handler.js
  2:3  warning  Unexpected console statement  no-console
  5:1  error    Delete `⏎`                    prettier/prettier

✖ 2 problems (1 error, 1 warning)
  1 error and 0 warnings potentially fixable with the `--fix` option.

                                                                                                                                                                                                                                           friendly-errors 11:40:31
 @ ./.nuxt/apollo-module.js 93:13-57
 @ ./.nuxt/index.js
 @ ./.nuxt/client.js
 @ multi eventsource-polyfill webpack-hot-middleware/client?reload=true&timeout=30000&ansiColors=&overlayStyles=&name=client&path=/__webpack_hmr/client ./.nuxt/client.js
                                                                                                                                                                                                                                           friendly-errors 11:40:31
ℹ Waiting for file changes                                                                                                                                                                                                                                 11:40:31
ℹ Memory usage: 433 MB (RSS: 559 MB)                                                                                                                                                                                                                       11:40:31

 ERROR  ENOSPC: System limit for number of file watchers reached, watch '/home/sky0621/src/github.com/sky0621/study-graphql/frontend/node_modules/@nuxt/vue-app/template/views/loading'                                                                    11:40:31

  at FSWatcher.start (internal/fs/watchers.js:165:26)
  at Object.watch (fs.js:1329:11)
  at createFsWatchInstance (node_modules/@nuxt/builder/node_modules/chokidar/lib/nodefs-handler.js:118:15)
  at setFsWatchListener (node_modules/@nuxt/builder/node_modules/chokidar/lib/nodefs-handler.js:165:15)
  at NodeFsHandler._watchWithNodeFs (node_modules/@nuxt/builder/node_modules/chokidar/lib/nodefs-handler.js:330:14)
  at NodeFsHandler._handleDir (node_modules/@nuxt/builder/node_modules/chokidar/lib/nodefs-handler.js:551:19)
  at runMicrotasks (<anonymous>)
  at processTicksAndRejections (internal/process/task_queues.js:93:5)
  at async NodeFsHandler._addToNodeFs (node_modules/@nuxt/builder/node_modules/chokidar/lib/nodefs-handler.js:600:16)

我被责备了。Prettier的检查已经进行了。
按照被告知的,进行lint修复。

$ yarn run lint --fix
yarn run v1.19.1
$ eslint --ext .js,.vue --ignore-path .gitignore . --fix

/home/sky0621/src/github.com/sky0621/study-graphql/frontend/plugins/apollo-error-handler.js
  2:3  warning  Unexpected console statement  no-console

✖ 1 problem (0 errors, 1 warning)

Done in 2.27s.

然后,成功启动了。
虽然似乎出现了一个警告,但是这是为了调试而在错误处理器中输出到控制台的日志吗?
假设在正式发布时会删除,所以暂时忽略它(反正也不会正式发布)。

  〜〜〜

↻ Updated plugins/apollo-error-handler.js                                                                                                                                                                                                                  11:45:05

✔ Client
  Compiled successfully in 694.86ms

✔ Server
  Compiled successfully in 823.17ms


 WARN  Compiled with 1 warnings                                                                                                                                                                                                            friendly-errors 11:45:07

Module Warning (from ./node_modules/eslint-loader/dist/cjs.js):                                                                                                                                                                            friendly-errors 11:45:07

/home/sky0621/src/github.com/sky0621/study-graphql/frontend/plugins/apollo-error-handler.js
  2:3  warning  Unexpected console statement  no-console

✖ 1 problem (0 errors, 1 warning)

                                                                                                                                                                                                                                           friendly-errors 11:45:07
You may use special comments to disable some warnings.                                                                                                                                                                                     friendly-errors 11:45:07
Use // eslint-disable-next-line to ignore the next line.                                                                                                                                                                                   friendly-errors 11:45:07
Use /* eslint-disable */ to ignore all warnings in a file.                                                                                                                                                                                 friendly-errors 11:45:07
screenshot-localhost-44171-2019.11.16-11-56-29.png

后台

创建用于存储后端源代码的目录以及用于存储GraphQL模式的目录。

$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql
$ tree -L 1
.
├── frontend
└── README.md

1 directory, 1 file
$ mkdir backend
$ tree -L 1
.
├── backend
├── frontend
└── README.md

Go项目初始化

$ cd backend/
$ go mod init github.com/sky0621/study-graphql/backend
go: creating new go.mod: module github.com/sky0621/study-graphql/backend$ tree -L 1
.
└── go.mod

0 directories, 1 file
$ cat go.mod 
module github.com/sky0621/study-graphql/backend

go 1.13

通过gqlgen命令生成骨架。

一下子就会自动生成各种文件。(甚至不需要编写GraphQL模式就能为你准备样例)

$ go run github.com/99designs/gqlgen init
Exec "go run ./server/server.go" to start GraphQL server
$
$ tree -L 2
.
├── generated.go
├── go.mod
├── go.sum
├── gqlgen.yml
├── models_gen.go
├── resolver.go
├── schema.graphql
└── server
    └── server.go

自动生成物品的检查

GraphQL 的模式

# GraphQL schema example
#
# https://gqlgen.com/getting-started/

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}

type User {
  id: ID!
  name: String!
}

type Query {
  todos: [Todo!]!
}

input NewTodo {
  text: String!
  userId: String!
}

type Mutation {
  createTodo(input: NewTodo!): Todo!
}

实际上,我们选择将主题设为“简易ToDo应用程序”是因为 gqlgen 默认会生成上述模式。
gqlgen 是一个以模式为基础的库,因此我们将根据此模式,结合仅在首次自动生成的源代码和每次运行 gqlgen 命令时自动生成的源代码,来实现功能。

解决者

使用GraphQL架构定义的查询(在这个例子中是 `todos: [Todo!]!`,即获取ToDo列表)和变更(在这个例子中是 `createTodo(input: NewTodo!): Todo!`,即创建新的ToDo)的逻辑实现的源代码。
※此源代码是由gqlgen命令自动生成的源代码(后续不会再次生成,除非删除该文件或者不修改gqlgen.yml配置)。

package backend

import (
    "context"
) // THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.

type Resolver struct{}

func (r *Resolver) Mutation() MutationResolver {
    return &mutationResolver{r}
}
func (r *Resolver) Query() QueryResolver {
    return &queryResolver{r}
}

type mutationResolver struct{ *Resolver }

func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) {
    panic("not implemented")
}

type queryResolver struct{ *Resolver }

func (r *queryResolver) Todos(ctx context.Context) ([]*Todo, error) {
    panic("not implemented")
}

当前的 panic(“not implemented”) 将被替换为由前端调用时执行的逻辑。
因此,进行了如下修正:

$ git diff
diff --git a/backend/resolver.go b/backend/resolver.go
index d015cbe..9bbe326 100644
--- a/backend/resolver.go
+++ b/backend/resolver.go
@@ -16,11 +16,38 @@ func (r *Resolver) Query() QueryResolver {
 type mutationResolver struct{ *Resolver }

 func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) {
-       panic("not implemented")
+       return &Todo{
+               ID:   "todo001",
+               Text: "部屋の掃除",
+               Done: false,
+               User: &User{
+                       ID:   "user001",
+                       Name: "たろー",
+               },
+       },nil
 }

 type queryResolver struct{ *Resolver }

 func (r *queryResolver) Todos(ctx context.Context) ([]*Todo, error) {
-       panic("not implemented")
+       return []*Todo{
+               &Todo{
+                       ID:   "todo001",
+                       Text: "部屋の掃除",
+                       Done: false,
+                       User: &User{
+                               ID:   "user001",
+                               Name: "たろー",
+                       },
+               },
+               &Todo{
+                       ID:   "todo002",
+                       Text: "買い物",
+                       Done: true,
+                       User: &User{
+                               ID:   "user001",
+                               Name: "たろー",
+                       },
+               },
+       },nil
 }

Go服务器启动逻辑

gqlgen命令自动生成了main函数和Web服务器启动逻辑的目标代码。
它在8080端口上启动,同时在根路径显示了GraphQL的Playground。(简直完美到不行)

package main

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

    "github.com/99designs/gqlgen/handler"
    "github.com/sky0621/study-graphql/backend"
)

const defaultPort = "8080"

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

    http.Handle("/", handler.Playground("GraphQL playground", "/query"))
    http.Handle("/query", handler.GraphQL(backend.NewExecutableSchema(backend.Config{Resolvers: &backend.Resolver{}})))

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

启动GraphQL服务器

$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql/backend
$
$ go run server/server.go 
2019/11/16 12:33:49 connect to http://localhost:8080/ for GraphQL playground
screenshot-localhost-8080-2019.11.16-12-38-57.png
screenshot-localhost-8080-2019.11.16-12-43-04.png

只是,这次在resolver.go中返回了一个固定值,所以不太在意,但从考虑到要持久化到RDB,我觉得后端的实现可能会变得困难一些。
实际上,我在工作中同时编写了GraphQL的前端和后端,但从信息交互的角度来说,后端要困难得多。
嗯,关于这一点,我计划在下一篇文章中编写与RDB连接的代码,到那时候再说吧。

好的,现在我们忽略对于Todo新注册的确认(因为和查询一样),我们只关注后端部分。
最后,我们需要调整前端,以连接后端的GraphQL服务器。

重新回到前端

使用Apollo建立GraphQL服务器连接。

准备查询文件

为了向GraphQL服务器发送请求,需要写一个查询,在后端的playground上试过了。
添加一个名为”apollo”的目录,其中包含一个文件夹结构,存放用于”Todo列表获取”的查询文件。

$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql/frontend
$
$ tree -L 1
.
├── apollo
├── assets
├── components
   ・・・
$
$ tree -L 2 apollo/
apollo/
└── queries
    └── todos.gql
$
$ cat apollo/queries/todos.gql 
query todos {
  todos {
    id
    text
    done
    user {
      id
      name
    }
  }
}

前端首页

修改为打开时显示Todo列表。

前端的目录结构如下:

$ tree -L 1
.
├── apollo
├── assets
├── components
├── layouts
├── middleware
├── node_modules
├── nuxt.config.js
├── package.json
├── pages
├── plugins
├── README.md
├── static
├── store
└── yarn.lock

1. 我们需要修改 pages/index.vue 这个文件。
2. 同时,我们还需要创建一个作为 components/TodoCard.vue 组件从 pages 中调用的组件。

<template>
  <div>
    <TodoCard />
  </div>
</template>

<script>
import TodoCard from '~/components/TodoCard.vue'

export default {
  components: { TodoCard }
}
</script>

以下的页面只需要调用组件就可以了。

我们使用以下的组件通过Apollo获取了Todo列表。

<template>
  <div>
    <v-row>
      <v-col cols="12" sm="6" offset-sm="3">
        <v-card>
          <v-list two-line subheader>
            <v-list-item v-for="todo in todos" :key="todo.id" link>
              <v-list-item-avatar>
                <v-icon>mdi-gift-outline</v-icon>
              </v-list-item-avatar>
              <v-list-item-content>
                <v-list-item-title>{{ todo.text }}</v-list-item-title>
                <v-list-item-subtitle>{{ todo.done }}</v-list-item-subtitle>
              </v-list-item-content>
              <v-list-item-content>
                <v-list-item-title>
                  {{ todo.user.name }}
                </v-list-item-title>
              </v-list-item-content>
            </v-list-item>
          </v-list>
        </v-card>
      </v-col>
    </v-row>
  </div>
</template>

<script>
import todos from '~/apollo/queries/todos.gql'
export default {
  data() {
    return {
      todos: []
    }
  },

  apollo: {
    todos: {
      prefetch: true,
      query: todos
    }
  }
}
</script>

在中国专业人士中,可以从事先准备好的扩展名为gql的文件中导入查询来与GraphQL服务器进行联系。

import todos from '~/apollo/queries/todos.gql'

在 nuxt-config.js 文件中,将 @nuxtjs/apollo 模块加载为配置,并通过以下方式在 Vue 文件中定义来将 GraphQL 服务器的查询结果存储在名为 todos 的数据中。

  apollo: {
    todos: {
      prefetch: true,
      query: todos
    }
  }

我决定使用Vuetify的v-card组件来进行一览显示(虽然v-data-table也可以)。
在上面的示例中,通过v-for循环遍历todos获取TODO列表并将每个项目显示出来,没有进一步的说明。

啊,对了。忘记了。GraphQL服务器的路径是”/query”,所以需要修改nuxt-config.js如下。

diff --git a/frontend/nuxt.config.js b/frontend/nuxt.config.js
index afc0db4..8454218 100644
--- a/frontend/nuxt.config.js
+++ b/frontend/nuxt.config.js
@@ -69,7 +69,7 @@ export default {
     clientConfigs: {
       default: {
         // Goサーバを 8080 ポートで起動する予定のため
-        httpEndpoint: 'http://localhost:8080/'
+        httpEndpoint: 'http://localhost:8080/query'
       }
     },
screenshot-localhost-35567-2019.11.16-15-04-51.png

总结

由于时间的关系,我省略了Todo的新注册,但是通过使用GraphQL连接前端和后端的实现案例,我能够展示出来。不过,在这个任务和编码水平下,可能还没有出现使用GraphQL的动力…
从前端的角度来看,当需要将多层次结构的信息作为列表显示在一个页面上时,以前需要为每个层次调用API,现在通过GraphQL库,能够一次获取所需的所有信息,并将其结构化,这对前端来说非常便利。
但是,相反地,后端却有很多需要考虑的问题,并且尽管GraphQL本身作为规范存在,但它并没有定义诸如”认证”、”分页”等具体的实现方式,所以根据所使用的库,可能需要自己重新发明一些轮子。
似乎还有像Prisma这样的框架(?)存在,但在没有更多广泛的实践和知识积累之前,在生产环境中使用还是有些可怕的…

本次的所有源代码如下:
https://github.com/sky0621/study-graphql/tree/v0.1.0

bannerAds