使用 Next.js , Nexus, Prisma 构建全栈项目
此教程翻译自《Complete Intruduction to Fullstack,Type-Safe GraphQL(feat.Next.js,Nexus,Prisma)》
我们将要从模板中构建全栈项目,会使用 Next.js ,Nexus,Prisma ,来实现 React 的后端渲染,GraphQL 接口。
技术栈浅析
首先,我们浅析一下我们选择的技术栈。
- TypeScript - 前后端使用统一的编程语言
- React 和 Next.js - 前端开发框架 React ,以及React 的服务端渲染库
- Urql GraphQL client - GraphQL 的客户端
- PostgreSQL - 数据库
- Nexus - 一个 code-first 的 GraphQL 服务端
- Prisma Client 和 Prisma Migrate - 用于数据库 ORM 操作的工具库(注意:Prisma Migrate 仍然在实验阶段)
现在,我们开始吧!
配置开发环境
在我们开始编写代码前,首先要配置开发环境所需要的软件,以便我们能够使用轻松的方式编写代码。
我们使用的软件编辑器是 VS Code , 它的插件库里提供了 Prisma 和 GraphQL 的代码高亮和自动格式化工具,我们先在 VS Code 中安装下面两个插件。
接下来,我们要安装 docker (我们的 PostgreSQL 数据库将在 docker 容器中运行,当然其他的云数据库或本地安装的数据库亦可),具体安装教程请前往 docker 官网。
注意事项
我们的开发工具是 mac pro 笔记本电脑,在 window 系统下的 pc 电脑运行结果如有不同,请先自行查找原因,然后在评论区留言讨论。 在命令行下运行指令,如无特别说明,一律是在项目根目录下运行。
新建一个 Next.js 项目
我们可以使用 ceate-next-app
工具包新建一个 Next.js 项目。在命令行工具输入下面的指令:
npx create-next-app prisma-next-nexus --use-yarn -e with-typescript
create-next-app
会使用 Git
自动下载项目初始化代码,并自动安装所有的依赖。指令运行完成以后,在 VS Code 打开项目目录,可以看到下图所示的项目结构。
安装 Nexus 和 Prisma
现在,我们安装 Nexus 框架和 nexus-plugin-prisma
包。在命令行工具中运行下面的指令:
yarn add nexus @prisma/client && yarn add @prisma/cli -D
Nexus 包含了提供 Typescript 语言类型编译支持的插件 Nexus TypeScript Language Service Plugin
,我们只需要在 Typescript 配置文件 tsconfig.json
中配置即可,修改代码如下:
{
"compilerOptions": {
// ...
"noEmit": true,
"rootDir": ".",
"typeRoots": ["node_modules/@types", "types"],
"plugins": [{ "name": "nexus/typescript-language-service" }] // 新增这一行
},
// ...
"include": ["**/*.ts", "**/*.tsx", ".", "types.d.ts"]
}
使用 Docker 启动数据库
我们使用 docker-compose
保存 postgresql 容器的设置,在项目根目录下添加 docker-compose-yml
文件,文件内容如下:
version: '3.1'
services:
db:
image: postgres:11.7
container_name: prisma-next-nexus-postgre
restart: always
environment:
POSTGRES_USER: prismaNextNexus
POSTGRES_PASSWORD: ${POSTGRESPWD}
ports:
- 54333:5432
volumes:
- ./db/postgresql:/var/lib/postgresql/data
其中 ${POSTGRESPWD}
是保存在 .env
文件中的数据库密码变量。同样的,在根目录下新建 .env
文件,并添加下面的代码:
POSTGRESPWD=xxxxxx // 密码
然后在命令行中输入如下指令:
docker-compose up -d
启动过程没有报错,即成功,命令会自动退出。 -d
时表示docker 容器在后台运行。
Prisma 连接数据库
我们要使用 Prisma 连接到上一步启动的 postgresql 数据库容器。首先,我们在根目录下新建 prisma
文件夹,然后在其中添加 .env
文件,然后在 .env
文件中添加环境变量 DATABASE_URL
,代码如下“
DATABASE_URL="postgresql://prismaNextNexus:xxx@localhost:54333/postgres"
别忘了替换数据库的密码
因为 .env
文件里包含了密码这样的敏感信息,不应该提交到 git 仓库中,所以在 .gitignore
中添加省略 .env
。
最后,我们需要在 /prisma
创建 schema 文件,用于设置数据库配置,和数据库 model 描述。具体代码如下:
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Hello {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
}
使用 Prisma 初始化数据库表结构
在 schema.prisma
写入的 model 描述就是数据库表的描述。我们接下来就要使用 @prisma/cli
工具在 数据库中自动创建,更新数据表。
在命令行工具中输入下面的指令:
yarn prisma migrate save --name init --experimental
运行结果如下图:
此时,Prisma 已经在 prisma
目录下创建了连接文件 migration
。
最后,将连接的信息推送到数据中使用下面的指令:
yarn run prisma migrate up --experimental
运行完成以后,可以在数据库中看到我们在 prisma.schema
定义的 Hello
。
我们使用的数据库 UI 工具是 TablePlus。
连接 Nexus 和 Next.js
Next.js 的 API routes 功能非常方便,我们只需要添加 /pages/api/graphql.ts
文件,然后启动 next.js 服务,就可以在 http://app-domain/api/graphql
访问 GraphqQL 服务。
在 graphql.ts
文件中编写下面的代码:
import app, { server } from "nexus";
import "../../graphql/schema"; // we'll create this file in a second!
app.assemble();
export default server.handlers.graphql;
下面,我们在根目录创建 graphql
目录,并且在这个目录下新建 schema.ts
文件。在 schema.ts
文件中,我们首先初始化 nexus-plugin-prisma
插件,设置 CRUD 功能,并通过 plugin 和 nexus 连接。具体代码如下:
import { use } from "nexus";
import { prisma } from "nexus-plugin-prisma";
use(prisma({ features: { crud: true } }));
最后,在命令行中启动 nexus 服务
yarn run nexus dev
此时,graphql 已经启动,访问 localhost:4000/graphql
可以看到 GraphQL Playground 已经运行(目前只有空的 schema)。
编写 GrahphQL API
现在,我们开始写一个 API。
定义对象类型
首先,我们在 graphql/shema.ts
中定义一个 User
对象类型,这里使用的 nexus 定义对象的语法:
schema.objectType({
name: "User",
definition(t) {
t.model.id();
t.model.name();
},
})
当你在 VS Code 中编写上面代码时,VS Code 将会自动完成这些字段( id
, name
)。这是因为我们已经在 prisma/schema.prisma
中定义好了 User
model。
现在,打开 Grapqhl Playground 并切换到 Schema 标签页。你会看到已经添加了 Grqphql 对象类型 User
。
定义 Query 类型
Nexus 使用 schema.queryType
函数来定义 root Query
。
我们写一个查询全部 User
的 query
- allUsers
。
具体代码如下:
schema.queryType({
definition(t) {
t.list.field("allUsers", {
type: "User",
resolve(_parent, _args, ctx) {
return ctx.db.user.findMany();
},
});
},
});
在 resolve
函数中,可以添加实际的逻辑代码。Prisma 客户端里可以操作 数据的实例 db
包含在上下文对象 ctx
中。 更多关于 Prisma 客户端 API 的信息可以在官网文档中了解。
Nexus-Prisma plugin 也包含了从数据库读取数据的方法,下面的代码可以直接定义读取 user
users
的 query。
schema.queryType({
definition(t) {
t.list.field("allUsers", {
type: "User",
resolve(_parent, _args, ctx) {
return ctx.db.user.findMany();
},
});
t.crud.user(); // nexus-prisma 定义的
t.crud.users(); // nexus-prisma 定义的
},
});
别忘了,查询单个 user 时传入参数的 Input
类型描述也会自动生成。
input UserWhereUniqueInput {
id: String
}
定义 Mutation 类型
举一反三,和 Query type 类似, Mutation
类型使用 schema.mutationType
类定义。
接下来,让我们来创建一个 bigRedButton
mutation 用来删除所有的 user 数据。代码如下:
schema.mutationType({
definition(t) {
t.field("bigRedButton", {
type: "String",
async resolve(_parent, _args, ctx) {
const { count } = await ctx.db.user.deleteMany({});
return `${count} user(s) destroyed.`;
},
});
},
});
同样的,nexus-prisma plutin 也为 mutation 提供了很多 CURD 函数。
schema.mutationType({
definition(t) {
t.field("bigRedButton", {
type: "String",
async resolve(_parent, _args, ctx) {
const { count } = await ctx.db.user.deleteMany({});
return `${count} user(s) destroyed.`;
},
});
t.crud.createOneUser(); // 创建一个用户
t.crud.deleteOneUser(); // 删除一个用户
t.crud.updateOneUser(); // 更新用户信息
t.crud.updateManyUser(); // 更新多个用户信息
},
});
自动生成的 GraphQL schema 如下:
type Mutation {
bigRedButton: String
createOneUser(data: UserCreateInput!): User!
deleteOneUser(where: UserWhereUniqueInput!): User
updateOneUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User
updateManyUser(
data: UserUpdateManyMutationInput!
where: UserWhereInput
): BatchPayload!
}
现在,我们完整的 GraphqlQL API 已经完成了!接下来,我们尝试在 React 中使用 GraphQL 发送请求进行增删改查。
在 React 中使用 GraphQL
我们将会在前端代码中使用 Urql 。当然,也可以使用其他你熟悉的 GrphQL 客户端,比如 Apollo client 等。
设置 Urql GraphQL 客户端
首先,在命令行工具运行下面的安装指令:
yarn add graphql-tag next-urql react-is urql isomorphic-unfetch
然后,在 /pages
目录下新增 _app.tsx
文件。这个文件是特殊的 Next.js 组件,它会在初始化每一个页面时都会被执行。
我们在 /pages/_app.tsx
文件中添加下面的代码:
import React from "react";
import { withUrqlClient, NextUrqlAppContext } from "next-urql";
import NextApp, { AppProps } from "next/app";
import fetch from "isomorphic-unfetch";
// the URL to /api/graphql
const GRAPHQL_ENDPOINT = `http://localhost:3000/api/graphql`;
const App = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
};
App.getInitialProps = async (ctx: NextUrqlAppContext) => {
const appProps = await NextApp.getInitialProps(ctx);
return { ...appProps };
};
export default withUrqlClient((_ssrExchange, _ctx) => ({
url: GRAPHQL_ENDPOINT,
fetch,
}))(
// @ts-ignore
App
);
现在,在前端所有页面都可以使用 GrphQL 客户端来发送网络请求了!
使用 GraphQL client 查询所有用户
首先,在根目录下创建 components
文件夹,用来保存所有的组件文件。在 components
文件夹下创建 AllUsers.tsx
。 AllUsers.tsx
文件里定义 React 函数组件,组件内部将会请求 allUsers
GraphQL query 并把查询返回的结果显示出来。
我们现在 AllUser.tsx
中定义 gql 查询语句,如下代码:
import gql from "graphql-tag";
const AllUsersQuery = gql`
query AllUsers {
allUsers {
id
name
}
}
`;
接下来,定义查询请求返回后的数据类型,我们知道返回数据是一个包好所有 User
对象的数组。所以,我们定义如下类型:
type AllUsersData = {
allUsers: {
id: string;
name: string;
}[];
}
最后,我们完成 React 函数组件,并发送请求获取所有的 User
数据。
import React from "react";
import {useQuery} from 'urql';
...
export default function AllUsers() {
const [result] = useQuery<AllUsersData>({ // 发送请求,更多 API 信息请查看 urql 官网
query: AllUsersQuery,
});
const { data, fetching, error } = result;
if (fetching) return <p>Loading...</p>;
if (error) return <p>Oh no... {error.message}</p>;
return (
<div>
<p>There are {data?.allUsers?.length} user(s) in the database:</p>
<ul>
{data?.allUsers?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
完整版的函数组件代码如下:
import React from "react";
import {useQuery} from 'urql';
import gql from "graphql-tag";
const AllUsersQuery = gql`
query AllUsers {
allUsers {
id
name
}
}
`;
type AllUsersData = {
allUsers: {
id: string;
name: string;
}[];
}
export default function AllUsers() {
const [result] = useQuery<AllUsersData>({ // 发送请求,更多 API 信息请查看 urql 官网
query: AllUsersQuery,
});
const { data, fetching, error } = result;
if (fetching) return <p>Loading...</p>;
if (error) return <p>Oh no... {error.message}</p>;
return (
<div>
<p>There are {data?.allUsers?.length} user(s) in the database:</p>
<ul>
{data?.allUsers?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
最后,我们在 /pages/index.tsx
主页文件中渲染 AllUsers 组件,代码如下:
import AllUsers from "../components/AllUsers";
const IndexPage = () => (
<div>
<h1>Hello Next.js </h1>
{/* === Tada! === */}
<AllUsers />
</div>
);
export default IndexPage;
接下来,我们在命令行启动 next.js App ~
yarn dev
最后,访问 localhost:3000
。
自动生成 useQuery
hooks 和 types
如果我们想要自动生成 GraphQL API 的类型描述而不是手动定义所有的,我们可以使用一个非常酷的工具 GraphQL Code Generator ,它可以从 Nexus GraphQL 入口 schema 描述中直接生成客户端所需的类型定义代码。这样,我们就只需在
schema.prisma
文件定义一次类型,然后,客户端所使用的类型都会自动通过 GraphQL Code Generator
自动生成。
首先,在命令行工具运行下面的代码来安装所需的包:
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript
@graphql-codegen/typescript-operations
@graphql-codegen/typescript-urql
然后,我们在根目录下添加 codegen.yml
配置文件来告诉 GraphQL Code Generator
的 GraphQL schema 访问地址等信息,我们项目的配置信息如下:
overwrite: true
schema: "http://localhost:4000/graphql" # GraphQL endpoint via the nexus dev server
documents: "graphql/**/*.graphql.ts" # parse graphql operations in matching files
generates:
generated/graphql.tsx: # location for generated types, hooks and components
plugins:
- "typescript"
- "typescript-operations"
- "typescript-urql"
config:
withComponent: false # we'll use Urql client with hooks instead
withHooks: true
然后,我们需要在 graphql
目录下新建 queries.graphql.ts
文件,并将 components/AllUsers.tsx
中的 gql 标签对象剪切到 queries.graphql.ts
中。具体代码如下图:
import gql from "graphql-tag";
export const AllUsersQuery = gql`
query AllUsers {
allUsers {
id
name
}
}
`;
最后,别忘了启动 nexus ,之后我们就可以在命令行运行如下指令,来自动生成类型定义了:
yarn graphql-codegen
如果希望开发过程中监听文件改变,自动生成类型定义的话,可以添加 --watch
参数。
指令运行成功以后,我们可以在产出目录 generated
中查看 graphql.tsx
文件内容,部分代码如下:
...
export function useAllUsersQuery(options: Omit<Urql.UseQueryArgs<AllUsersQueryVariables>, 'query'> = {}) {
return Urql.useQuery<AllUsersQuery>({ query: AllUsersDocument, ...options });
};
...
最后,我们在 components/AllUsers.tsx
中使用生成 useAllUsersQuery
函数来发送请求获取用户数据。代码如下:
import React, { useEffect } from "react";
import {
useAllUsersQuery,
} from "../generated/graphql";
export default function AllUsers() {
const [result] = useAllUsersQuery(); // 直接调用函数即可
const { data, fetching, error } = result;
if (fetching) return <p>Loading...</p>;
if (error) return <p>Oh no... {error.message}</p>;
return (
<div>
<p>There are {data?.allUsers?.length} user(s) in the database:</p>
<ul>
{data?.allUsers?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
结尾
我们希望你能喜欢这篇实践教程,并学习到如何配置 prisma,nexus,nextjs 来完成全栈的开发!
你可以访问此 Github repo 下的 prisma-next-nexus
目录来查看完整代码。