使用Next.js和StepZen进行实战,开发GraphQL的现代化应用
首先
通过使用Next.js和StepZen来开发待办事项应用程序,本资料旨在提供GraphQL在构建现代Web应用程序时的方法。希望对选择使用GraphQL时的技术选择提供参考。即使不使用StepZen,也可以参考使用Apollo Client和GraphQL Code Generator开发Next.js应用程序的方法。
目标人群
-
- Web サービスを迅速に立ち上げたい方
-
- GraphQL によるモダンなアプリ開発に関心がある方
- Web サービスの技術選定を検討中の方
先决条件
参与者需要具备基本的Web应用开发知识。
这个实操的内容
做的事情 (zuò de
-
- Next.js と StepZen での todo アプリ作成
-
- StepZen を利用しての GraphQL API サーバーの高速構築
-
- GraphQL Code Generator でのクライアントコードの自動生成
- Apollo Client を使用した GraphQL のクライアント実装
不做的事情
-
- React/Next.js や StepZen の詳細説明
-
- 認証・認可やセキュリティ関連の実装
-
- エラーハンドリングの実装
- Vercel を利用した Web アプリのデプロイ
请参考我之前写的文章关于StepZen的介绍(GraphQL/StepZen 手把手教程 #1 – 知乎)。
创建应用程序
完成此实践后,您将创建一个具备以下功能的“待办事项”应用程序。
-
- Todo 一覧表示: すべての todo アイテムを一覧できるページ。
-
- Todo 追加機能: 新しい todo アイテムを追加できるフォーム。
-
- Todo 編集機能: 既存の todo アイテムの完了・未完了を変更する機能。
-
- Todo 削除機能: 任意の todo アイテムを削除することができるボタン。
- データベースとの連携: ユーザーが入力した todo アイテムは PostgreSQL に保存され、アプリケーションの再起動後もデータは維持されます。
完成的应用程序的形象
此外,您可以通过以下的 GitHub 链接访问源代码,请参考:optimisuke/todoapp-nextjs-stepzen:使用 Next.js 和 StepZen 的待办事项应用程序。
应用程序的配置
使用的技术和服务
-
- 開発言語・フレームワーク: Node.js, Next.js, React, TypeScript
-
- データベース: PostgreSQL
-
- ツール & ライブラリ: GraphQL Code Generator, Apollo Client, MUI
- マネージドサービス: StepZen (RDB や REST API を GraphQL API に変換), Neon (PostgreSQL のマネージドサービス)
简要流程
我們將按照以下步驟進行實作。
-
- 用Node.js安装
-
- 使用Next.js/React构建todo应用界面
-
- 使用PostgreSQL/Neon构建todo数据库
-
- 使用StepZen构建GraphQL API服务器
-
- 使用GraphQL Code Generator自动生成客户端代码
- 从Next.js/React调用GraphQL API
详细说明的步骤
1. 安装 Node.js
请从以下网址下载安装程序并安装。
下载 | Node.js
还有其他的方法可以使用Volta或Homebrew进行安装。请根据您自己的环境进行安装。
使用Next.js/React构建待办事项应用程序界面
接下来,我们将使用Next.js/React来构建应用程序界面。在这里,尚未调用后端API。
首先,在工作文件夹中使用create-next-app工具来建立环境。
npx create-next-app@latest --typescript
会有一些互动问题,但在默认设置下没有问题。
在工作目录中创建一个名为my-app的文件夹,当进行前端(Next.js / React)工作时,请在my-app文件夹中运行命令。
为了进行操作确认,请运行以下命令。
cd my-app
npm run dev
请在浏览器中访问 http://localhost:3000/,您将看到以下样式的 Next.js 初始界面,请进行确认。如有需要,请使用 npm run dev 命令并在需要时按下 Ctrl-C 终止。
然后,安装我们本次要使用的软件包。
主要选择了与 MUI 相关的内容。
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
然后,删除app文件夹,创建pages文件夹和components文件夹。接下来,创建以下4个文件。
-
- pages/index.tsx
-
- components/TodoList.tsx
-
- components/TodoInput.tsx
- components/TodoItem.tsx
就四个文件的内容来说,我们从这里开始解释。
首先,作为顶层页面,在下面的文件中显示标题和 TodoList 组件。
我们使用了 MUI 组件来调整外观。
import { Typography, Container } from "@mui/material";
import React from "react";
import TodoList from "../components/TodoList";
const IndexPage: React.FC = () => {
return (
<Container maxWidth="sm">
<Typography variant="h4" align="center" gutterBottom>
ToDo List
</Typography>
<TodoList />
</Container>
);
};
export default IndexPage;
接下来,在TodoList组件中,我们将展示TodoInput组件和TodoItem组件的列表。另外,还需使用state来管理任务,并编写添加、复选框切换和删除的函数。
添加函数将传递给TodoInput组件,而切换和删除的函数将传递给TodoItem组件。
import { List } from "@mui/material";
import React, { useState } from "react";
import TodoItem from "./TodoItem";
import TodoInput from "./TodoInput";
type Task = {
task: string;
completed: boolean;
};
const TodoList: React.FC = () => {
const [tasks, setTasks] = useState<Task[]>([]);
const addTask = (task: string) => {
setTasks([...tasks, { task, completed: false }]);
};
const toggleTask = (index: number) => {
const newTasks = [...tasks];
newTasks[index].completed = !newTasks[index].completed;
setTasks(newTasks);
};
const deleteTask = (index: number) => {
const newTasks = [...tasks];
newTasks.splice(index, 1);
setTasks(newTasks);
};
return (
<div>
<TodoInput addTask={addTask} />
<List>
{tasks.map((task, index) => (
<TodoItem
key={index}
task={task.task}
completed={task.completed}
toggleTask={() => toggleTask(index)}
deleteTask={() => deleteTask(index)}
/>
))}
</List>
</div>
);
};
export default TodoList;
TodoInput 组件使用表单来执行添加操作。同时,它将输入的字符串作为状态进行管理,并进行显示。
import { TextField, Button, Box } from "@mui/material";
import React, { useState } from "react";
type TodoInputProps = {
addTask: (task: string) => void;
};
const TodoInput: React.FC<TodoInputProps> = ({ addTask }) => {
const [task, setTask] = useState("");
const submitTask = (e: React.FormEvent) => {
e.preventDefault();
if (task === "") return;
addTask(task);
setTask("");
};
return (
<form onSubmit={submitTask}>
<Box display="flex" justifyContent="center" gap="10px" p={2}>
<TextField
label="New task"
variant="outlined"
value={task}
onChange={(e) => setTask(e.target.value)}
/>
<Button type="submit" variant="contained" color="primary">
Add
</Button>
</Box>
</form>
);
};
export default TodoInput;
在 TodoItem 组件中,我们展示了复选框、todo 任务的标题和删除图标。
import {
ListItem,
ListItemText,
IconButton,
ListItemSecondaryAction,
} from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
import RadioButtonUncheckedIcon from "@mui/icons-material/RadioButtonUnchecked";
type TodoItemProps = {
task: string;
completed: boolean;
toggleTask: () => void;
deleteTask: () => void;
};
const TodoItem: React.FC<TodoItemProps> = ({
task,
completed,
toggleTask,
deleteTask,
}) => {
return (
<ListItem>
<IconButton edge="start" color="inherit" onClick={toggleTask}>
{completed ? <CheckCircleOutlineIcon /> : <RadioButtonUncheckedIcon />}
</IconButton>
<ListItemText
primary={task}
style={{ textDecoration: completed ? "line-through" : "none" }}
/>
<ListItemSecondaryAction>
<IconButton edge="end" color="inherit" onClick={deleteTask}>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
);
};
export default TodoItem;
请使用以下命令进行操作确认。
npm run dev
当连接到 http://localhost:3000 时,我认为会显示以下屏幕。
使用PostgreSQL/Neon构建todo数据库。
到目前为止,我已经只用前端创建了一个 Todo 应用程序。从下一步开始,我将准备数据库并设置以便能够永久保存数据。
Neon 是一个针对 PostgreSQL 的托管服务。虽然有一些限制,但在免费额度下非常容易使用。
请按照以下步骤逐步创建数据库。
使用下面的 SQL 创建表。将 SQL 复制到之前的 SQL 编辑器画面上,并按下 Run 按钮执行 SQL。
这次,表只包含基本信息。同时,我们决定使用 UUID 作为 id。
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE todos (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
title VARCHAR(255) NOT NULL,
completed BOOLEAN DEFAULT false NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
稍后,为了确认表中的内容,也会将数据存入。与之前一样,将使用SQL编辑器执行以下SQL语句。
INSERT INTO todos (title, completed)
VALUES ('新しいToDo', false);
另外,我们将注册触发器,以便自动更新 update_at。
虽然有几个选项可以在何处生成 update_at 的值,但由于后端服务器(API服务器)在 StepZen 上自动生成逻辑不太容易,而且前端(客户端)可能依赖于用户环境,因此我们决定在数据库中进行更新。
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_name_before_update
BEFORE UPDATE ON todos
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
现在,数据库的构建已经完成。接下来,我们将使用StepZen来将数据库转化为GraphQL API。
使用StepZen构建GraphQL API服务器。
StepZen是一项能够将数据库等简单部署为GraphQL API服务器的云服务。我们可以通过浏览器上的仪表板和命令行界面来进行操作。首先需要创建账号并安装命令行工具,然后使用命令行来构建GraphQL API服务器。
首先,让我们访问StepZen的官方网站并创建一个账户。
接下来,我们将安装StepZen CLI。
然后,在Next.js/React的my-app文件夹旁边创建一个名为api的文件夹,并在其中进行操作。
mkdir api
cd api
使用StepZen CLI,您可以直接从本地计算机访问StepZen服务。要安装CLI,请运行以下npm命令(假设已安装Node.js)。添加-g参数以进行全局安装,安装命令可在任何位置运行,不会有问题。
npm install -g stepzen
如果发生包含”permission denied”错误的情况,可以考虑是文件夹权限问题。请尝试使用”sudo npm install -g stepzen”命令来解决。
安装完成后,您可以使用以下命令登录到StepZen。登录命令可以在任何位置执行,不会有任何问题。
stepzen login
请在下面的提示框中输入StepZen仪表板的密钥图标中获取的账户名称和管理员密钥。
What is your account name?: ACCOUNT_NAME
What is your admin key?: ADMIN_KEY
通过这一步骤,StepZen的设置已经完成。
接下来,执行以下命令以生成GraphQL模式和用于StepZen构建的配置文件。请在创建的名为api的StepZen文件夹内执行该命令。
stepzen import postgresql
我们要按照以下对话要求输入必要的项目。
我们将输入StepZen的用户信息和终端点名称,以及在Neon中确认的连接信息。在Neon中,连接信息被记载为postgres://username:password@host/databasename,所以请将username、password、host、databasename复制到问题中,并填写每个信息。
另外,将问题有关架构的回答设置为默认的”default”,最后一个问题的回答设置为”Yes”,我们输入”y”。
$ stepzen import postgresql
? What would you like your endpoint to be called? api/todos
stepzen import postgresql - introspect a PostgreSQL database and extend your GraphQL schema with the types, queries and mutations for accessing it through a StepZen API.
? What is your host? xxx.example.com
? What is the username? optimisuke
? What is the password? [hidden]
? What is your database name? neondb
? What is your database schema (leave blank to use defaults)?
? Automatically link types based on foreign key relationships using @materializer
(https://stepzen.com/docs/features/linking-types) Yes
Starting... done
Successfully imported postgresql data source into your GraphQL schema
接下来,我们将对postgresql/index.graphql进行修改。
我们将对插入操作进行修改,使id和update_at不再必需。
title项被设为必需,而completed项则是可选的。
我们考虑了completed项不存在的情况,并使用了SQL函数COALESCE。
以下是修改前后的摘录部分。
在原有的变异周围(第54行,类型为Mutation的中间部分)
insertTodos(
id: ID!
completed: Boolean
created_at: DateTime
title: String!
updated_at: DateTime
): Todos
@dbquery(
type: "postgresql"
schema: "public"
table: "todos"
dml: INSERT
configuration: "postgresql_config"
)
改动后
insertTodos(title: String!, completed: Boolean): Todos
@dbquery(
type: "postgresql"
schema: "public"
query: """
INSERT INTO todos (title, completed) VALUES (
$1,
COALESCE($2, false)
)
RETURNING *
"""
configuration: "postgresql_config"
)
当准备好后,请在api文件夹中使用以下命令进行部署。
stepzen start
每次在修改保存模式下执行部署的命令。如有需要,请使用Ctrl-C停止。您可以在StepZen仪表板的资源管理器界面上确认已创建指定模式的API。
现在,GraphQL API已经构建完成。接下来,我们将通过修改客户端的Next.js/React代码来实现调用GraphQL API的功能。
使用GraphQL Code Generator生成客户端代码。
准备获取所需数据的查询,并使用GraphQL Code Generator生成客户端代码。
在接下来的步骤中,我们将进入第二章中创建的my-app文件夹,并开始进行操作。
cd my-app
首先,您在StepZen仪表板上进行操作并准备查询。
创建一个执行CRUD操作的查询,将文件放在在第2章中创建的my-app文件夹的components文件夹中。
query getTodos {
todosList {
id
title
completed
created_at
updated_at
}
}
mutation updateTodo($id: ID!, $completed: Boolean, $title: String) {
updateTodos(id: $id, completed: $completed, title: $title) {
completed
created_at
id
title
updated_at
}
}
mutation insertTodo($title: String!, $completed: Boolean) {
insertTodos(title: $title, completed: $completed) {
completed
created_at
id
title
updated_at
}
}
mutation deleteTodo($id: ID!) {
deleteTodos(id: $id) {
completed
created_at
id
title
updated_at
}
}
下一步是安装GraphQL Code Generator所需的东西。
npm install graphql @graphql-codegen/client-preset
npm i -D @graphql-codegen/cli
准备设置文件。根据查询的.graphql文件和StepZen上的模式文件,生成包含类型信息等代码的./src/gql/文件夹。
import { CodegenConfig } from "@graphql-codegen/cli";
import * as dotenv from "dotenv";
dotenv.config();
const apiKey = process.env.NEXT_PUBLIC_API_KEY || "invalid-api-key";
const uri = process.env.NEXT_PUBLIC_API_URI || "invalid-api-uri";
const config: CodegenConfig = {
schema: [
{
[uri]: {
headers: {
Authorization: `Apikey ${apiKey}`,
},
},
},
],
documents: ["./**/*.graphql"],
ignoreNoDocuments: true, // for better experience with the watcher
generates: {
"./src/gql/": {
preset: "client",
},
},
};
export default config;
将以下内容写入.env文件中,包括的是模式的URL和API密钥。
NEXT_PUBLIC_API_URI=https://xxx.stepzen.net/api/todos/__graphql
NEXT_PUBLIC_API_KEY=xxx::stepzen.net+1000::yyy
您可以从StepZen的下面界面获取API密钥。
将自动生成命令的快捷方式添加到package.json的scripts中。
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"codegen": "graphql-codegen-esm --config codegen.ts"
},
使用以下命令进行自动生成操作。
npm run codegen
确认在my-app/src/gql/中生成了代码。
6. 使用 Next.js/React 发起 GraphQL API 调用
使用从 Next.js/React 代码自动生成的客户端,调用使用 StepZen 构建的 GraphQL API。
先安装所需的软件包。
npm install @apollo/client
首先,我们将添加一个pages/_app.tsx文件,并在其中添加Apollo Client,以便能够使用它。
API密钥和GraphQL API的模式URI将从.env文件中获取,与congen.ts相同。
import { ApolloProvider } from "@apollo/client";
import { AppProps } from "next/app";
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
const apiKey = process.env.NEXT_PUBLIC_API_KEY || "invalid-api-key";
const uri = process.env.NEXT_PUBLIC_API_URI || "invalid-api-uri";
const httpLink = new HttpLink({
uri: uri,
});
const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
authorization: `Apikey ${apiKey}`,
},
};
});
const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: authLink.concat(httpLink),
});
function MyApp({ Component, pageProps }: AppProps) {
return (
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
);
}
export default MyApp;
另外,我们将对 TodoList.tsx 进行如下更改。使用 @apollo/client 的 useQuery 和 useMutation 来访问 GraphQL API,同时指定由 GraphQL Code Generator 生成的对象作为参数。此外,在添加、修改和删除时,我们调用 update 函数来更新从查询中获取的 todo 数组的缓存。这段代码可能看起来有点复杂,但请参考 Apollo 的官方文档(Apollo GraphQL Docs)进行确认。
import { List } from "@mui/material";
import React from "react";
import TodoItem from "./TodoItem";
import TodoInput from "./TodoInput";
import { useMutation, useQuery } from "@apollo/client";
import {
DeleteTodoDocument,
GetTodosDocument,
InsertTodoDocument,
UpdateTodoDocument,
} from "@/src/gql/graphql";
const TodoList: React.FC = () => {
const { data, loading, error } = useQuery(GetTodosDocument);
const [insertTodo] = useMutation(InsertTodoDocument, {
update(cache, { data }) {
const existingData = cache.readQuery({ query: GetTodosDocument });
cache.writeQuery({
query: GetTodosDocument,
data: {
todosList: [...existingData?.todosList!, data?.insertTodos!],
},
});
},
});
const addTask = (task: string) => {
insertTodo({ variables: { title: task } });
};
const [updateTodo] = useMutation(UpdateTodoDocument, {
update(cache, { data }) {
const existingData = cache.readQuery({ query: GetTodosDocument });
const updatedTodos = existingData?.todosList?.map((todo) =>
todo?.id === data?.updateTodos?.id ? data?.updateTodos! : todo
);
cache.writeQuery({
query: GetTodosDocument,
data: { todosList: updatedTodos },
});
},
});
const toggleTask = (index: string, completed: boolean) => {
updateTodo({ variables: { id: index, completed: !completed } });
};
const [deleteTodo] = useMutation(DeleteTodoDocument, {
update(cache, { data }) {
const existingData = cache.readQuery({ query: GetTodosDocument });
const updatedTodos = existingData?.todosList?.filter(
(todo) => todo?.id !== data?.deleteTodos?.id
);
cache.writeQuery({
query: GetTodosDocument,
data: { todosList: updatedTodos },
});
},
});
const deleteTask = (index: string) => {
deleteTodo({ variables: { id: index } });
};
return (
<div>
<TodoInput addTask={addTask} />
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
<List>
{data?.todosList?.map((task, index) => (
<TodoItem
key={index}
task={task?.title!}
completed={task?.completed!}
toggleTask={() => toggleTask(task?.id!, task?.completed!)}
deleteTask={() => deleteTask(task?.id!)}
/>
))}
</List>
</div>
);
};
export default TodoList;
以上是,Next.js/React 代碼的修改已完成。
請使用下面的命令執行並進行操作確認。
npm run dev
尽管首次版本与未调用GraphQL API的版本相同,并且在从Neon的Tables中查看数据时,可以看到数据确实被持久保存了。总之,本次工作坊已经结束了。
最后
我相信通过这个实践项目,我们可以学习到使用最先进的技术,如Next.js、GraphQL和StepZen等,来开发现代化的Web应用程序的方法。通过按照实际步骤构建应用程序并体验整个流程,我们将能够在未来的技术选择和项目启动中受益。如果有任何问题或建议,请在评论栏中提出。谢谢!