使用”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

后台
创建用于存储后端源代码的目录以及用于存储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


只是,这次在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'
}
},

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