datetime类型怎么输入_GraphQL 前后端实践与工具链选配(ts类型推断)以及我放弃使用的 GraphQL 的八大理由

出于改变世界的责任感

某天看一眼 idea 列表,就随手挑个大胆的想法,准备实现一把。

大胆的想法配大胆的技术,awesome~~~

下面我就来介绍一下,前后端结合 GraphQL 来进行开发的具体实践,并搭配一套相对舒适的开发工具链(自动化代码生成、完善语法提示)

前言

GraphQL 是一门语言规范,本身不提供具体实现。在实际开发中需要自己选择适合的第三方实现。

项目涉及前后端,一家人要整整齐齐,在全局上选取了 apollo 作为具体实现

作为工具类文章,我在下面列出了涉及到的主要模块,用于索引。可以直接使用 command + f 搜索关键词来快速定位,节选自己感兴趣的部分进行阅读。

前端,重点介绍 GrahpQL 工具链的选配(vscode 插件、graphql 代码自动生成工具),如何进行舒适的开发。

后端,则重点介绍 type-graphql 与 typeorm 的整体使用,以贴代码为主。

在最后,我会谈谈在实践中发现的问题,我对于 GraphQL 现阶段的看法。以及你真的需要 GraphQL 吗?

本文不是从零开始,如果遇到困难,或需要完整源码,可以在评论区留言,改日更新到文章内。
  • 前端
    • next
    • @apollo/client
    • @graphql-codegen/cli
  • 后端
    • apollo-server-express
    • type-graphql
    • typeorm
    • typedi
  • 开发工具(vscode)
    • VSCode GraphQL(插件)

前端

前端使用 @apollo/client 来进行数据获取,搭配 graphql-codegen 进行代码自动生成,以及 VSCode GraphQL 插件的语法提示

那么 GraphQL 查询语句 怎么写 ?写在哪里?怎么在业务中去用?

又到展示我高超的画图技巧的环节,下面让我来画个图说明流程

f271a8ff782cb3990abe798912eadbf8.png

在上面的开发流程中,每个阶段我们都能获取完整的类型提示,从 graphql 提示,到 ts 类型提示。

首先是 vscode 插件,直接在 vscode 插件中心搜索 graphql,第一项就是我们需要使用的插件

0171e034d61c82ffb088c3183b9e7d68.png

根据说明,在项目根目录配置 .graphqlconfig.yml

# 此处也可以直接使用 schema: 'http://localhost:4000/api/graphql' 配置为可访问的graphql服务
schemaPath: "./server/schema.graphql"
includes: ["**/*.{graphql,gql,ts,tsx,js}"] # 将为那些文件提供语法提示
extensions:
  endpoints:
    default: http://localhost:4000/api/graphql # 配置后可以直接进行 test 测试

配置好后,如果代码提示没有生效,可以尝试 shift + command + p 唤出菜单,选择restart 重启插件

991c8bf37d68859d6df59dccb29a1087.png

下面编写查询语句,具体语法参考 官网

~/query.graphql

fragment knodeFields on Knode {
  id
  name
  parentId
  gitDir
  readme
  gitOwner
  gitRepo
}

query getKnodes {
  knodes {
    ...knodeFields
  }
}

query getKnode($id: String!) {
  knode(id: $id) {
    ...knodeFields
  }
}

mutation addKnode($data: AddKnode) {
  addKnode(data: $data) {
    ...knodeFields
  }
}

安装代码自动生成工具

npm i @graphql-codegen/cli  @graphql-codegen/introspection @graphql-codegen/near-operation-file-preset  @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo 

在根目录配置 codegen.yml

overwrite: true
schema: "./server/schema.graphql" # 可以提供可用的 graphql 服务
documents: "./genapi/**/*.graphql" # 你编写的 graphql 查询语句
generates:
  genapi/index.tsx: # 生成文件的输出路径
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo

# generates: # 这里提供了一种,就近生成代码的配置,比如 a/query.graphql 生成 a/query.gen.tsx
#   types.ts:
#     plugins:
#       - typescript
#   pages:
#     documents: "./pages/**/*.graphql"
#     preset: near-operation-file
#     presetConfig:
#       extension: .gen.tsx
#       baseTypesPath: ../types.ts
#     plugins:
#       - typescript-operations
#       - typescript-react-apollo

graphql-codegen --config codegen.yml -w 开始生成、启动监听

接着看看生成后的代码长啥样,根据配置,我的生成路径在 genapi/index.tsx

import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null;

...略

export type Knode = {
  __typename?: 'Knode';
  gitDir?: Maybe<Scalars['String']>;
  gitOwner?: Maybe<Scalars['String']>;
  gitRepo?: Maybe<Scalars['String']>;
  id?: Maybe<Scalars['String']>;
  name?: Maybe<Scalars['String']>;
  parentId?: Maybe<Scalars['String']>;
  readme?: Maybe<Scalars['String']>;
};

export type Mutation = {
  __typename?: 'Mutation';
  updateKnode?: Maybe<Knode>;
};

...略

/**
 *
 * @example
 * const [updateKnodeMutation, { data, loading, error }] = useUpdateKnodeMutation({
 *   variables: {
 *      id: // value for 'id'
 *      data: // value for 'data'
 *   },
 * });
 */
export function useUpdateKnodeMutation(baseOptions?: Apollo.MutationHookOptions<UpdateKnodeMutation, UpdateKnodeMutationVariables>) {
        return Apollo.useMutation<UpdateKnodeMutation, UpdateKnodeMutationVariables>(UpdateKnodeDocument, baseOptions);
      }
export type UpdateKnodeMutationHookResult = ReturnType<typeof useUpdateKnodeMutation>;
export type UpdateKnodeMutationResult = Apollo.MutationResult<UpdateKnodeMutation>;
export type UpdateKnodeMutationOptions = Apollo.BaseMutationOptions<UpdateKnodeMutation, UpdateKnodeMutationVariables>;
export const GetKelementsDocument = gql`
    query getKelements($knodeId: String, $date: DateTime, $searchText: String, $hasKnode: Boolean = false) {
  kelements(knodeId: $knodeId, date: $date, searchText: $searchText) {
    ...kelementFields
    knode @include(if: $hasKnode) {
      ...knodeFields
    }
  }
}
    ${KelementFieldsFragmentDoc}
${KnodeFieldsFragmentDoc}`;

...略

超酷,直接基于我们的原始查询语句,生成了 apollo react hook 组件。

来看看怎么用,如下

import { useUpdateKnodeMutation, useGetKnodesQuery } from "genapi"; // 引入

export default function Home() {
  const { data, loading, error, refetch } = useGetKnodesQuery();
  const [updateKnode] = useUpdateKnodeMutation();

  return (
    <div
      onClick={async () => {
        await updateKnode({
          variables: {
            id: '1',
            data: {
              name: '老王',
              parentId: '2',
            },
          },
        });
        refetch();
      }}
    >
      name: {data.knodes[0].name}
    </div>
  );
}

简单贴图说明一下这超酷的开发过程

161547ca25302b6db6885370dde0e710.png
使用提示

5c3848b70206e864b0c2e910595a27de.png
输入提示

63a1a704b68daf71734576a913ce8038.png
输入提示

5f031234939c1b94fb598208e9aa2877.png
错误提示

在工具链的选择上,尝试过一些其他方案,也在下面简单列一下。

1.以字符串的形式将 GraphQL 直接写在 js/ts 业务代码中,利用插件给予提示

缺点:提示不完善、与业务代码耦合重、无法得到完整的 ts 类型推断

2.直接写 GraphQL 查询语句,通过graphql-tag/loader增加 webpack 支持,最后直接导入到业务代码中进行使用

缺点:本质还是导入字符串,无法得到完整的 ts 类型推断

后端

项目入口 app.ts,使用 apollo-server-express 提供基础服务

import { schema } from "./apollo/schema";
import { ApolloServer } from "apollo-server-express";
import * as express from "express";

const app = express();

// app.use(async (req, res, next) => {
//   await dbConnect(); // 可以使用 express 写一些插件,用于提供自定义服务
//   next();
// });

app.listen({ port: 4000 }, () =>
  console.log(`  Server ready at http://localhost:4000/api/graphql`)
);

const server = new ApolloServer({
  schema,
});

server.applyMiddleware({
  app,
  path: "/api/graphql",
  cors: {
    origin: "http://localhost:3000",
    credentials: true,
    allowedHeaders: ["Content-Type", "Authorization"],
  },
});

~/apollo/schema.ts,使用type-graphql构建服务所需的schema

import "reflect-metadata";
import { buildSchemaSync } from "type-graphql";
import * as path from 'path'

export const schema = buildSchemaSync({
  // resolvers: [KnodeResolver], 
  resolvers: [path.resolve(__dirname, "./**/index.ts")], // 支持多种形式来载入rsolvers
  // 项目启动时,输出一份 schema.graphql 文件到目录,我们后面工具链会用到
  emitSchemaFile: "schema.graphql", 
  nullableByDefault: true,
  // authChecker, // 可以在这配置 type-graphql 默认提供的鉴权服务
  // container: Container, // 依赖注入相关
  // globalMiddlewares: [auth], // 配置全局中间件
});

resolvers 部分目录结构为

├── knode
│   ├── index.ts
│   ├── dto.ts
│   └── model.ts

~/apollo/knode/index.ts

import { Resolver, Query, Mutation, Arg, Authorized, Ctx } from "type-graphql";
import { Knode } from "./model";
import { AddKnode, UpdateKnode } from "./dto";
import { Service } from "typedi";

// @Service() // 依赖注入
@Resolver(() => Knode)
export default class KnodeResolver {
  // constructor(private readonly gitService: GitService) {} // 注入服务

  // @Authorized() // 鉴权
  @Query(() => Knode)
  async knode(@Arg("id") id: string) {
    return Knode.findOne(id);
  }

  @Mutation(() => Knode)
  async addKnode(@Arg("data") data: AddKnode, @Ctx() { uid }) { // uid是利用express插件机制注入的
    // await this.gitService.createGitdir(uid, data.gitDir); 调用注入的服务
    const knode = Knode.create(data); // Knode 同时是typeorm的实例,所以可以直接使用
    knode.uid = uid;
    await knode.save();
    return knode;
  }

  @Mutation(() => Knode)
  async updateKnode(@Arg("id") id: string, @Arg("data") data: UpdateKnode) {
    const knode = await Knode.findOne({ where: { id } });
    Object.assign(knode, data);
    const newKnode = await knode.save();
    return newKnode;
  }

  @Mutation(() => Boolean)
  async delKnode(@Arg("id") id: string) {
    const book = await Knode.findOne({ where: { id } });
    await book.remove();
    return true;
  }
}

补充一下,如果期望不进行返回值的校验,可以自己提供类型

import { GraphQLScalarType } from "graphql";
export const AnyType = new GraphQLScalarType({
  name: "any",
  description: "any type",
  parseValue(value: any) {
    return value; // value from the client input variables
  },
  serialize(value: any) {
    return value; // value sent to the client
  },
  parseLiteral(ast) {
    return null;
  },
});

定义后就可以,@Mutation(()=> AnyType)

~/apollo/knode/dto.ts 提供 Mutation 的字段校验

import { InputType, Field } from "type-graphql";
import { IsNotEmpty } from "class-validator";

@InputType()
export class AddKnode {
  @Field()
  @IsNotEmpty()
  name: string;

  @Field()
  parentId: string;

  @Field()
  gitDir: string;

  @Field()
  readme: string;
}

@InputType()
export class UpdateKnode {
  @Field()
  name: string;

  @Field()
  parentId: string;

  @Field()
  gitDir: string;

  @Field()
  readme: string;
}

~/apollo/knode/model.ts 直接将 typeorm 与 graphql 统一定义

import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
import { ObjectType, Field } from "type-graphql";

@Entity()
@ObjectType()
export class Knode extends BaseEntity { // 直接继承 BaseEntity,便于使用
  @Field()
  @PrimaryGeneratedColumn("uuid")
  id: string;

  @Field()
  @Column()
  uid: string;

  @Field()
  @Column()
  name: string;

  @Field()
  @Column({ name: "parent_id" })
  parentId: string;

  @Field()
  @Column("text")
  readme: string;

  @Field()
  @Column({ nullable: true })
  gitDir?: string;

  ...
}

补充一下,关于 typeorm 的配置

import { getConnectionManager } from "typeorm";
import config from "config";

export const dbConnect = async () => {
  const connectionManager = getConnectionManager();

  if (connectionManager.has("default")) {
    const connection = connectionManager.get("default");

    if (!connection.isConnected) {
      await connection.connect();
    }
    return connection;
  } else {
    return connectionManager
      .create({
        ...config.db,
        entities: ["apollo/**/model.ts"],
        migrations: [],
      } as any)
      .connect();
  }
};

// 参考
// const config.db = {
//   type: "mysql",
//   host: "localhost",
//   port: 3306,
//   username: "root",
//   password: ".Jqz1996",
//   database: "wuku",
//   synchronize: true,
//   logging: false,
// }

补充二,一个有意思的点,起初是我打算直接在 next.js 提供的 api 路由内使用 typeorm,然后遇到问题。typeorm 如何在热更新的情况下使用?可以参考下面,每次热更新检查是否已存在实例

function entitiesChanged(prevEntities: any[], newEntities: any[]): boolean {
  if (prevEntities.length !== newEntities.length) return true;

  for (let i = 0; i < prevEntities.length; i++) {
    if (prevEntities[i] !== newEntities[i]) return true;
  }

  return false;
}

async function updateConnectionEntities(
  connection: Connection,
  entities: any[]
) {
  if (!entitiesChanged(connection.options.entities, entities)) return;

  (connection.options as any).entities = entities;

  (connection as any).buildMetadatas();

  if (connection.options.synchronize) {
    await connection.synchronize();
  }
}

export async function ensureConnection() {
  const connectionManager = getConnectionManager();

  if (connectionManager.has("default")) {
    const connection = connectionManager.get("default");

    if (!connection.isConnected) {
      await connection.connect();
    }

    if (process.env.NODE_ENV !== "production") {
      await updateConnectionEntities(connection, Object.values(entities));
    }
    return connection;
  } else {
    return connectionManager
      .create({
        ...config.db,
        entities: Object.values(entities),
        migrations: [],
      } as any)
      .connect();
  }
}

以上,后端部分也已经介绍完毕。

如果你运行了项目,应该会看到项目目录下,多出了一个 schema.graphql文件,内容可能类似下面的。

这是 type-graphql 自动生成的,主要用于“看”,或者提供给其他工具链使用,可以用于代码提示或生成(当然,也可以直接使用 graphql 服务,启动服务同样可以提供支持)

# -----------------------------------------------
# !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!!
# !!!   DO NOT MODIFY THIS FILE BY YOURSELF   !!!
# -----------------------------------------------

...省略


input AddKnode {
  gitDir: String
  name: String
  parentId: String
  readme: String
}

input UpdateKnode {
  gitDir: String
  name: String
  parentId: String
  readme: String
}


"""any type"""
scalar any

"""
The javascript `Date` as string. Type represents date and time as the ISO Date string.
"""
scalar DateTime


type Knode {
  gitDir: String
  gitOwner: String
  gitRepo: String
  id: String
  name: String
  parentId: String
  readme: String
  uid: String
}

type Mutation {
  addKnode(data: AddKnode): Knode
  delKnode(id: String): Boolean
  updateKnode(data: UpdateKnode, id: String): Knode
}

type Query {
  knode(id: String): Knode
  knodes: [Knode]
}

最后

终于到最后了

来谈谈 GraphQL 实际使用的感受

  • 工具链不完善,官方没有提供合适的最佳实践,所以花了大量时间在工具链的打造上。并且这个流程目前并不完善,依旧有致命缺陷(你发现了吗)。
  • 复杂度过高,给人以黑盒的感觉、不直观,例如 apollo 的缓存机制,虽然照着文档理解了一番,但使用过程中依旧会遇到特殊问题。
  • 资料缺失,整体社区给人以割裂感。使用的 apollo type-graphql 虽然已经是主流,但依旧会遇到各种难解、无解的问题。
  • GraphQL schema 不容易设计,无法利用原有经验,实际开发中会发现,shema 很难设计好,与 restful 相比,缺乏社区实践,让你无法应用原有的经验来快速开发。
  • 后端工作量远大于前端,后端工作量远大于前端,例如会遇到 n + 1 性能问题,需要耗费更多的精力来开发。除非打钱,否则后端不会同意使用GraphQL的。
  • 极大的增加了系统复杂度,虽然上面提过了复杂度过高,这里再强调一遍,例如解析过程中出现特殊异常,问题极难定位。
  • 迁移成本高,整体架构与restful不兼容,迁移有工作量。
  • 学习成本高,使用初期会耗费大量时间在语法与填坑上,然后开发过程也同样是一个填坑的过程,总能遇到千奇百怪的问题。

说到这里,很难想象,我当初竟然是出于懒得写接口,快速开发 demo 的想法来使用GraphQL。。。我现在的心情是后悔,及其后悔,非常后悔。

以至于产生了写这篇文章的动力,我计划放弃在后续的 demo 开发中使用 graphql,为了有始有终,写下这篇文章,用于总结。

最后的问题,我也在这里回答下

你真的需要使用 GraphQL吗 ?(仅以个人角度出发,大公司或技术积累强的团队忽视即可)

  • 如果是为了快速开发 demo,放弃使用 GraphQL 的念头!
  • 如果是准备用于公司商业项目,放弃使用 GraphQL 的念头!
  • 学习目的,欢迎踩坑!
  • 内部项目,可以试错,欢迎踩坑!
  • 已有技术积累,欢迎踩坑!

以上

欢迎点赞收藏:)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值