使用 Urql、Apollo-server4、Prisma 实现端到端类型安全

Published on
5 mins read
––– views
graphql-urql-prisma

在本篇文章中,我将通过制作一个博客访问人次的小功能说明使用 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