我想要前后端可以共享数据库 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项目,可以推广这一种方法。我们可以把几种东西都分开,可以践行关注点分离的思想。而且有完备的类型提示和校验。
或许你有什么别的想法?欢迎在下方留言一起讨论!