我想要前后端可以共享数据库 schema 的类型,这样的话,在更改数据库 schema 后。我们不需要修改类型文件,直接使用 drizzle 生成的类型即可。本文我们就一起看看如何实现这个功能吧!
首先我们要准备一个 momorepo 的环境,目前使用 pnpm 可以很方便的实现。
初始化 monorepo 环境
创建一个 pnpm-workspace.yaml
文件,配置如下:
packages: - 'packages/*' - 'apps/*'
创建 shared 包用来放置前后端公用的代码
我在 packages 中放置可以复用的包。我的数据库schema文件放在 packages/shared
中。并使用 tsdown
来作为打包工具使用。
{ "name": "@labs/shared", "type": "module", "version": "1.0.0", "description": "前后端共享的包", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.js", }, }, "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ "dist", ], "scripts": { "build": "tsdown", "dev": "tsdown --watch", "lint": "eslint . --fix", }, "dependencies": { "drizzle-orm": "^0.44.4", "drizzle-zod": "^0.8.3", "zod": "^4.0.17", }, }
import { defineConfig } from 'tsdown' export default defineConfig({ entry: ['./src/index.ts'], dts: true, format: ['esm'], outDir: 'dist', shims: true, target: 'es2020', platform: 'browser', external: [ 'node:*', ], })
创建数据库 schema 文件并导出
接下来我们就可以在 src/schame
中写我们的 drizzle
了,我这里就用一个user的schema来做示例
我们这里规定所有schema文件都要以 entity.ts
结尾,方面后面使用
import { relations } from 'drizzle-orm'; import { pgTable, serial, varchar } from 'drizzle-orm/pg-core'; import { createInsertSchema, createSelectSchema } from 'drizzle-zod'; import { z } from 'zod'; import { timestamps } from './global'; export const user = pgTable('user', { id: serial().primaryKey(), username: varchar().notNull().unique(), password: varchar().notNull(), ...timestamps, }); export const selectUserSchema = createSelectSchema(user); export type SelectUser = z.infer<typeof selectUserSchema>; export const insertUserSchema = createInsertSchema(user) export type InsertUser = z.infer<typeof insertUserSchema>
timestamps内容如下
我们几乎每个表都会有创建时间和修改时间,抽离出来方便复用
import { timestamp } from 'drizzle-orm/pg-core'; export const timestamps = { createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), };
我们把它们都导出
import { user } from './user.entity' export * from './user.entity' // 这里组装好并导出方便在API端直接使用 export const schema = { user }
export * from './schema' // 导出schema的配置和类型,供其他包使用
然后就可以构建使用了,在开发环境我们可以使用刚才编写好的 dev
脚本(它会使用 tsdown --watch 监听文件的变化实时编译),打包时使用 build
脚本。
在这个monorepo仓库中的其他包如果要使用,就使用 pnpm add --workspace @labs/shared
安装,安装后就可以导入使用啦。
在其他环境中使用
我这里有一个fetch的包用来放置所有的API请求文件,使用tanstack-query作为请求库。一个API包用来创建所有接口。我们分别在两个包中安装刚才创建好的 @labs/shared
包。
我这里就展示一些重要的代码,具体代码就不展示了。在API端,我们需要根据导出的schema来创建连接。在fetch包中,我们需要导出的类型来进行请求或者响应的类型约束,方便前端对接时使用。
在 API 端使用
import 'dotenv/config'; import { defineConfig } from 'drizzle-kit'; export default defineConfig({ schema: '../../packages/shared/src/schema/*.entity.ts', out: './drizzle/migrations', dialect: 'postgresql', dbCredentials: { url: process.env.DATABASE_URL!, }, });
创建drizzle migrate需要的文件
import { schema } from '@labs/shared' import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; import env from '@/utils/env' const connectionString = env.database.DATABASE_URL; const pool = new Pool({ connectionString }); const db = drizzle(pool, { schema }) export default db
这样就可以使用了,我们可以使用 npx drizzle-kit generate
生成数据库迁移文件,并使用 npx drizzle-kit migrate
来进行数据库迁移。其余增删改查的操作就和以前一样,只不过schema相关的文件记得要在 @labs/shared
中导入。
pnpm monorepo 和 ts 结合起来的话编辑器会自动提示,这个还是很方便的。
在 fetch 包中使用
import type { InsertUser, SelectUser } from '@labs/shared' import type { AxiosError } from 'axios' import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' import type { IResponse } from './use-axios' // 这里构建响应都有的类型,使用泛型可以方便的将不一致的地方进行类型填充 // 查询用户列表 export function useGetUserListQuery() { const { axiosInstance } = useAxios() return useQuery<IResponse<SelectUser[]>, AxiosError>({ queryKey: ['userList'], queryFn: async () => { const response = await axiosInstance.get('/v1/user') return response.data }, }) } // 注册 export function useRegisterMutation() { const { axiosInstance } = useAxios() return useMutation<IResponse<{ accessToken: string, refreshToken: string }>, AxiosError, InsertUser>({ mutationFn: async (data: InsertUser) => { const response = await axiosInstance.post('/v1/auth/register', { data }) return response.data }, }) }
这里抽离fetch包的作用是如果你有两个或者几个不同的端,可以同时使用这个包进行对API的请求。同事们或者你自己使用的时候可以只关注请求那个接口,请求的过程和处理的过程会在这个包中完成。
这样做的好处
我们把数据库schema和它对应的生成的类型都抽离到 packages/shared
中,如果要用到相关的类型就直接用它生成的就好了,没必要再重新生成一份。到时候其他的类型相关的包也可以放到这里,在这里类型修改后,前后端使用到的地方都会进行类型校验并进行错误提示。
请求相关的包都要抽离到 packages/fetch
包中,如果哪个地方要请求API,你就可以使用这个包了,所有API端暴露出来的接口都会在这里使用tanstack query操作一次返回,你可以不用管请求的细节,只关注传参和结果即可。而且使用tanstack query,他会自动生成响应式数据,有请求状态的变化和接口缓存,之前手动封装的许多包就可以废弃掉了。
因为这些包都有ts的类型限制,所以在API响应改变或者请求改变后,这里改了后用的地方都会有类型不一致的提示,配合构建前进行类型校验,我们可以方便的找出错误,避免在API改变后忘记更改前端请求参数或者响应造成的错误。
总结
有了这套东西后,如果你的公司使用node作为BFF层或者你开一个新的node项目,可以推广这一种方法。我们可以把几种东西都分开,可以践行关注点分离的思想。而且有完备的类型提示和校验。
或许你有什么别的想法?欢迎在下方留言一起讨论!