在本篇文章中,我将通过制作一个博客访问人次的小功能说明使用 Urql、Apollo-server4、Prisma 实现数据库到后端再到前端的类型安全的全流程
使用 prisma 创建数据库模型
首先,我们要使用 prisma 创建我们的数据库模型。由于文章是 markdown 存储在 git 中,只需要定义一个 Views 模型统计文章标题和访问数量就可以了。
# 初始化prisma
pnpm prisma init
配置 prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model BlogPostViews {
id Int @id @default(autoincrement())
postName String @unique
views Int @default(0)
}
pnpm prisma generate
在 package.json 中添加 script
"scripts": {
"postinstall": "prisma generate",
}
添加 schema.graphql
由于需要生成类型且需要在 nextjs 的服务端里读取,所以单独将 graphql 的 schema 文件放在 public 目录下,如果有更好的方式也可以留言告诉我,谢谢。
type Query {
addViewCount(postName: String!): BlogPostViews!
}
type Mutation {
views(postName: String!): Int!
}
type BlogPostViews {
postName: String!
views: Int!
}
graphql 前后端类型生成
配置 codegen.ts 文件,执行 codegen 命令,在 src/gql 下生成 ts 类型
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: 'public/schema.graphql',
documents: ['src/**/*.tsx'],
ignoreNoDocuments: true, // for better experience with the watcher
generates: {
'./src/gql/': {
preset: 'client',
plugins: [],
},
'./src/gql/resolvers-types.ts': {
plugins: ['typescript', 'typescript-resolvers'],
config: {
useIndexSignature: true,
},
},
},
}
export default config
pnpm graphql-codegen --config codegen.ts
使用 Apollo Server 构建 GraphQL 服务端
接下来,我们将使用 Apollo-server 构建一个 GraphQL 服务端。我们将把 prisma 接入到 resolvers 中,这样我们就可以通过 GraphQL API 来查询数据库了。
Apollo-server 实例在 nextjs 提供的 pages/api 中初始化。
数据库到服务端的类型由 Prisma 来保证。
通过传入刚刚生成的类型 Resolvers,可以对编写的 resolvers 进行类型约束,使其满足我们 graphql 的 schema 的要求。
// src/pages/api/graphql.ts
import { ApolloServer } from '@apollo/server'
import { startServerAndCreateNextHandler } from '@as-integrations/next'
import { prisma } from '@/server/db'
import { Resolvers } from '@/gql/resolvers-types'
const typeDefs = readFileSync(join(process.cwd(), 'public', 'schema.graphql'), {
encoding: 'utf-8',
})
const resolvers: Resolvers = {
Query: {
async addViewCount(parent, { postName }) {
const result = await prisma.blogPostViews.findUnique({
where: { postName },
})
if (!result) {
throw new Error(`Blog post with name "${postName}" not found`)
}
return result
},
},
Mutation: {
async views(parent, { postName }) {
const blogPostViews = await prisma.blogPostViews.findUnique({
where: { postName },
})
let views = 1
if (blogPostViews) {
views = blogPostViews.views + 1
await prisma.blogPostViews.update({
where: { id: blogPostViews.id },
data: { views: views },
})
} else {
await prisma.blogPostViews.create({ data: { postName: postName, views: views } })
}
return views
},
},
BlogPostViews: {
async postName(blogPostViews) {
return blogPostViews.postName
},
async views(blogPostViews) {
return blogPostViews.views
},
},
}
const server = new ApolloServer({
typeDefs,
resolvers,
})
export default startServerAndCreateNextHandler(server)
urql 接入 nextjs 页面
urql 具有很好的可扩展性,并且可以轻松地集成到 React 应用程序中。
- 简单易用的 API
- 支持 SSR(服务端渲染)和 SSG(静态网站生成)
- 支持缓存和数据的预取
实例化 Urql:
// src/pages/_app.tsx
import { Provider as UrqlProvider } from 'urql'
import { Client, cacheExchange, fetchExchange } from 'urql'
const urqlClient = new Client({
url: '/api/graphql',
exchanges: [cacheExchange, fetchExchange],
})
export default function App() {
return <UrqlProvider value={urqlClient}>text</UrqlProvider>
}
通过导入我们在 src/gql 下生成的 graphql 函数,传入模板字符串,就可以确保 useMutation 有类型支持
// src/components/viewCounter.tsx
import { useEffect } from 'react'
import { useMutation } from 'urql'
import { graphql } from '@/gql'
const mutation = graphql(`
mutation Mutation($postName: String!) {
views(postName: $postName)
}
`)
export default function ViewCounter({ path, className }: { path: string; className: string }) {
const [result, executeMutation] = useMutation(mutation)
const views = Number(result.data?.views)
useEffect(() => {
executeMutation({ postName: path })
}, [executeMutation, path])
return <span className={className}>{`${views > 0 ? views.toLocaleString() : '–––'} views`}</span>
}
结语
以上介绍了在 graphql 下实现端到端类型安全的具体实现,可以在我的博客项目里查看代码。
如果你并未使用 graphql,并且也想在同构项目中实现端到端类型安全,可以使用 trpc