前言
TypeScript + React 类型安全三件套:Component、Redux、和Service类型化。
Service-Graphql 类型化
Graphql 是 Facebook 推出的接口查询语言——比较成熟且主流的前后端交互技术方案之一。
Graphql 的定位
大概是在19年9月的时候,我决定在技术探索性项目里使用 Graphql——但如何把 Graphql 整合到“类型安全三件套”的技术体系里,却一直没有一个比较满意的技术方案。
在 React 里边,Graphql 比较经典的使用方式,有将 Graphql Schema 和 Query 转换成高阶组件或者 Hooks,但这其实破坏了我当时正大力推行的单向数据流【Redux + useReducer】基本技术方案。
最终,经过反复思考,我觉得把 Graphql 定位为 Restful API 的替代方案之一,把它当做纯粹的 Service 层来认知和处理,这样 Graphql 和既有基本技术方案的冲突是最小,从而能无缝的整合进来。
Graphql 的类型化
Graphql 语言本身
作为 TypeScript 的深度使用者,肯定也希望编写 Graphql 的时候,也能实现静态类型检测——结构、类型是否正确,这些低级错误,我们希望有一个途径能够实时的检测并抛出来。
很幸运的,类似于 TypeScript,有一个 Graphql Language Server,可以通过 Schema 检测我们编写的 Query 语句是否有错误。
已 VSCode 编辑器为例,我们可以:
- 安装 Prisma.vscode-graphql 插件
- 安装 watchman
- 在项目根目录里配置 .graphqlconfig,比如配置:
{
"schemaPath": "schema/schema.graphql",
// @IMP: 踩过坑,一定要 "**/*",不然不生效
"includes": ["**/*.graphql", "**/*.gql"],
"excludes": ["node_modules"],
"extensions": {
"endpoints": {
"dev": "http://127.0.0.1/graphql",
"prod": "http://www.naotu.com/graphql"
}
}
}
这样就可以实现对 Query 语句的实时静态检测。
Tips:这个插件现在有个坑就是不能动态监测schema的变化——shema变化,暂时可能需要重启vscode才行。
gen Service
同样,我们需要一个工具将接口文档 Schema 和编写的 Query 语句转成 TypeScript Service 调用代码,这也有开源的方案和工具 @graphql-codegen。
安装依赖:
npm i -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-graphql-request
配置 codegen.yaml:
schema: ./schema/schema.graphql
documents: ./src/**/*.graphql
generates:
src/models/service.ts:
plugins:
# 引入本插件以生成符合规范的 Service 代码
- 'node_modules/@bit/ks-ef-common.graphql-codegen-tkit-plugin'
config:
operationResultSuffix: Res
src/models/types.d.ts:
plugins:
- typescript
- typescript-operations
config:
globalNamespace: true
flattenGeneratedTypes: true
exportFragmentSpreadSubTypes: true
# immutableTypes: true
# avoidOptionals: true
operationResultSuffix: Res
因为默认的 @graphql-codegen/typescript-graphql-request 提供的 Service 模板并不是基础技术方案所需要的,所以通过 graphql-codegen-tkit-plugin 插件对 typescript-graphql-request 进行修改:
const {
GraphQLRequestVisitor,
validate,
plugin
} = require('@graphql-codegen/typescript-graphql-request');
const TypeTpl = `import { NonStandardAjaxPromise, ExtraFetchParams } from '@tkit/ajax';`;
const newPlugin = function() {
// eslint-disable-next-line prefer-rest-params
const args = [].slice.call(arguments, 0);
// eslint-disable-next-line prefer-spread
const { prepend, content } = plugin.apply(null, args);
return {
prepend: prepend.map(i => i.replace(/graphql-request/g, '@tkit/ajax/lib/graphql')),
content:
TypeTpl +
content
.replace(/\): Promise</g, ', opt?: ExtraFetchParams): NonStandardAjaxPromise<')
.replace(/variables\)/g, 'variables, opt)')
.replace(/graphql-request/g, '@tkit/ajax/lib/graphql')
.replace(/\( ,/, '(')
};
};
module.exports = {
GraphQLRequestVisitor,
plugin: newPlugin,
validate
};
将 "gql": "graphql-codegen"
添加到 package.json,执行:
npm run gql
即可生成类型化的 Service 代码
service.ts 示例:
import { GraphQLClient } from '@tkit/ajax/lib/graphql';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { NonStandardAjaxPromise, ExtraFetchParams } from '@tkit/ajax';
export const DocumentFragmentDoc = gql`
fragment Document on Doc {
id
name
owner
type
is_shared
parent_id
modify_time
snapshot_id
}
`;
export const DocListDocument = gql`
query docList($id: String!, $withDocId: Boolean = false) {
doc(id: $id) @include(if: $withDocId) {
...Document
}
docList(id: $id) {
...Document
}
}
${DocumentFragmentDoc}
`;
...
types.d.ts 示例:
export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export interface Scalars {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
}
export interface DocDocSnapshotArgs {
id: Scalars['String'];
}
...
参考资料
- vscode-graphql
- @graphql-codegen
- @divyenduz/ts-graphql-plugin 一个 TypeScript 插件